mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-18 15:39:47 +02:00
## Thinking Path > - Paperclip manages work for AI agents. > - Planning guidance helps agents choose owners and dependencies. > - The runtime skill favors few tasks, but the catalog skill requires a child-task breakdown. > - Both add repeated process instructions that can distract from the requested outcome. > - This change keeps the ownership and dependency rules and removes the required matrix and repeated checklist. > - A bounded Product E2E comparison measures saved outcomes and task handoffs before qualification. ## Linked Issues or Issue Description Refs #11057. Related measurement work: #15218. **What existing behavior does this improve?** Planning and delegation through the runtime plan-to-tasks and bundled task-planning skills. **Current behavior** The two skills contain about 1,900 words and conflicting guidance on whether plans require child tasks. **Proposed behavior** Keep cohesive work with one owner. Split only for a real owner, parallel output, dependency, independent review, or follow-up lifecycle. Preserve existing authorization and planning mechanics. ## What Changed - Shorten both skills to about 400 words combined. Preserve their keys and installed-version behavior. - Remove the duplicate operational-skill pointer and regenerate affected source metadata. - Add twelve explicit Product E2E cells: four scenarios with current, short and disabled planning skills. - Use the current task composer and actual create-response ID; calibrate public skill APIs and browser creation without providers. - Eliminate an observed collision in chat-test company prefixes with a per-suite sequence. - Grade saved documents, exact author/run attribution, child count, prerequisite execution order, review boundaries and completion handoffs. - Retain current skill bytes and report source, selections, run accounting and failures. ## Verification - `pnpm test:e2e:runner:typecheck`: pass. - `pnpm test:e2e:runner:unit`: 1,287 Vitest tests and 128 Node checks pass. - `pnpm test:e2e:runner -- --list --suite plan-task-guidance`: twelve local Codex cells. - Archived current skills match master `72ff3a9f27e581a27acb49771e8658bbb0bbaa47` exactly. - Corrected fixture: three real public-API/database calibrations pass with zero provider runs; all 35 evaluator checks and Product E2E typecheck pass. - Setup campaign [37399550253](https://github.com/paperclipai/paperclip/actions/runs/37399550253) was canceled after source review found unsupported bundled edits and automatic core reinstallation. Its paid-cell step was skipped: zero provider runs, no behavioral grade. - The next setup [37401094799](https://github.com/paperclipai/paperclip/actions/runs/37401094799) failed before task creation on the old title-field selector: zero actual runs, original FAIL retained, cleanup passed. A real browser/API calibration of the new helper passes with paused non-provider agents and zero runs. - Full local typecheck/build pass. Full local tests retain one unchanged five-minute Git streaming timeout (also fails isolated), 9,591 passes and 5,796 skips. CI's chat failure was a proven random fixture-prefix collision; five affected cases pass after the test-only repair. - Paid behavior comparison and new-head CI/review remain pending. This PR remains a draft. ## Risks - The shorter text may change delegation decisions. Live outcomes are not yet qualified. - The initial comparison uses one profile and one attempt per cell. It cannot establish cross-model reliability or cost trends. - Disabled means unassigned company-owned copies; the company library remains discoverable. This does not qualify global removal, automatic accepted-plan wiring changes, or installed-copy migration. - Skill availability does not prove a model read or cognitively used it. - No provider/tool protocol, permission, timeout or runtime lifecycle behavior changes in production. ## Model Used OpenAI Codex (GPT-6), with repository inspection, code editing and tool use. The exact backend model ID and context-window size are not exposed in this session. The declared eval model is native Codex `gpt-5.6-sol`. ## 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 - [ ] 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 - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
105 lines
6.7 KiB
TypeScript
105 lines
6.7 KiB
TypeScript
import { randomUUID } from "node:crypto";
|
|
import { mkdtemp, readFile, rm, stat } from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import express from "express";
|
|
import request from "supertest";
|
|
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
import { agents, companies, createDb, heartbeatRuns } from "@paperclipai/db";
|
|
import { companySkillRoutes } from "../routes/company-skills.js";
|
|
import { agentRoutes } from "../routes/agents.js";
|
|
import { errorHandler } from "../middleware/error-handler.js";
|
|
import { getEmbeddedPostgresTestSupport, startEmbeddedPostgresTestDatabase } from "./helpers/embedded-postgres.js";
|
|
import { preparePlanSkills, selectPlanSkills, verifyPlanSkills, verifyPlanSelection } from "../../../tests/runner-e2e/plan-task-skills.js";
|
|
import { planInvocations, planInvocationPrompt } from "../../../tests/runner-e2e/plan-task-exposure.js";
|
|
import { nativeTaskSkillInputs } from "../../../packages/paperclip-runner/src/backends/runtime-context.js";
|
|
import { resolveRunnerdCodexSkillInputs } from "../../../packages/paperclip-runner/src/live/runnerd-codex-transport.js";
|
|
import type { NativeRuntimeContextSnapshot } from "../../../packages/paperclip-runner/src/contracts/runtime-context.js";
|
|
import { PLAN_SKILLS, PLAN_VARIANTS, planHash } from "../../../tests/runner-e2e/plan-task-cases.js";
|
|
import type { RunnerApi } from "../../../tests/runner-e2e/api.js";
|
|
|
|
const support = await getEmbeddedPostgresTestSupport();
|
|
if (!support.supported) console.warn(`Planning fixture API calibration skipped: ${support.reason}`);
|
|
(support.supported ? describe : describe.skip)("planning fixture public API calibration (no providers)", () => {
|
|
let tempDb: Awaited<ReturnType<typeof startEmbeddedPostgresTestDatabase>>;
|
|
let db: ReturnType<typeof createDb>;
|
|
let root: string;
|
|
const savedHome = process.env.PAPERCLIP_HOME, savedInstance = process.env.PAPERCLIP_INSTANCE_ID;
|
|
let api: Pick<RunnerApi, "get" | "post">;
|
|
let app: express.Express;
|
|
beforeAll(async () => {
|
|
tempDb = await startEmbeddedPostgresTestDatabase("paperclip-plan-fixture-");
|
|
root = await mkdtemp(path.join(os.tmpdir(), "paperclip-plan-fixture-home-"));
|
|
process.env.PAPERCLIP_HOME = root;
|
|
process.env.PAPERCLIP_INSTANCE_ID = "default";
|
|
db = createDb(tempDb.connectionString);
|
|
app = express();
|
|
app.use(express.json());
|
|
app.use((req, _res, next) => { (req as any).actor = { type: "board", source: "local_implicit", isInstanceAdmin: true, userId: "local-board" }; next(); });
|
|
app.use("/api", companySkillRoutes(db));
|
|
app.use("/api", agentRoutes(db));
|
|
app.use(errorHandler);
|
|
api = {
|
|
async get<T>(url: string): Promise<T> {
|
|
const response = await request(app).get(url);
|
|
if (response.status !== 200) throw new Error(`GET ${url}: ${response.status} ${JSON.stringify(response.body)}`);
|
|
return response.body;
|
|
},
|
|
async post<T>(url: string, body?: unknown): Promise<T> {
|
|
const response = await request(app).post(url).send(body as object);
|
|
if (response.status < 200 || response.status >= 300) throw new Error(`POST ${url}: ${response.status} ${JSON.stringify(response.body)}`);
|
|
return response.body;
|
|
},
|
|
};
|
|
});
|
|
afterAll(async () => {
|
|
if (savedHome === undefined) delete process.env.PAPERCLIP_HOME; else process.env.PAPERCLIP_HOME = savedHome;
|
|
if (savedInstance === undefined) delete process.env.PAPERCLIP_INSTANCE_ID; else process.env.PAPERCLIP_INSTANCE_ID = savedInstance;
|
|
await tempDb?.cleanup();
|
|
if (root) await rm(root, { recursive: true, force: true });
|
|
});
|
|
it.each(PLAN_VARIANTS)("admits %s through real creation, readback and selection APIs", async variant => {
|
|
const companyId = randomUUID(), agentId = randomUUID();
|
|
await db.insert(companies).values({ id: companyId, name: "Planning calibration", issuePrefix: `P${companyId.slice(0, 6)}` });
|
|
await db.insert(agents).values({ id: agentId, companyId, name: "Fixture Lead", role: "general", adapterType: "paperclip_runner", adapterConfig: {} });
|
|
const before = await api.get<any[]>(`/api/companies/${companyId}/skills`);
|
|
const bundled = before.find(s => s.key === PLAN_SKILLS[0].key);
|
|
expect(bundled).toBeDefined();
|
|
const sources = await preparePlanSkills(api, companyId, variant);
|
|
expect(sources).toHaveLength(2);
|
|
expect(sources.every(s => s.key.startsWith(`company/${companyId}/eval-`))).toBe(true);
|
|
const selected = await selectPlanSkills(api, companyId, agentId, sources);
|
|
const invocations = planInvocations(sources, selected);
|
|
const contextSkills = [];
|
|
for (const source of sources.filter(s => s.selected)) {
|
|
const entry = (selected as any).entries.find((e: any) => e.key === source.key && e.desired);
|
|
const bytes = await readFile(path.join(entry.sourcePath, "SKILL.md"));
|
|
expect(planHash(bytes)).toBe(source.sha256);
|
|
const mode = (await stat(path.join(entry.sourcePath, "SKILL.md"))).mode & 0o555;
|
|
expect(mode).toBe(0o444);
|
|
const digest = planHash(JSON.stringify([{ path: "SKILL.md", sha256: planHash(bytes), mode, size: bytes.length }]));
|
|
expect(invocations.find(i => i.name === entry.runtimeName)?.assetDigest).toBe(digest);
|
|
contextSkills.push({ key: source.key, runtimeName: entry.runtimeName, versionId: entry.currentVersionId,
|
|
bundle: { rootPath: path.join(root, "runtime-context-assets/bundles", digest) } });
|
|
}
|
|
const context = { skills: contextSkills } as NativeRuntimeContextSnapshot;
|
|
const inputs = nativeTaskSkillInputs(planInvocationPrompt("Business task", invocations), context);
|
|
expect(inputs).toHaveLength(variant === "disabled" ? 0 : 2);
|
|
expect(inputs.map(i => i.name)).toEqual(invocations.map(i => i.name));
|
|
const providerHome = path.join(root, "isolated-codex-home");
|
|
expect(resolveRunnerdCodexSkillInputs(inputs, context, providerHome)).toEqual(inputs.map(i => ({
|
|
...i, path: path.join(providerHome, "skills", i.name, "SKILL.md"),
|
|
})));
|
|
await verifyPlanSelection(api, companyId, agentId, sources);
|
|
await verifyPlanSkills(api, companyId, sources);
|
|
const after = await api.get<any[]>(`/api/companies/${companyId}/skills`);
|
|
expect(after.find(s => s.key === bundled.key)?.id).toBe(bundled.id);
|
|
expect(after.find(s => s.key === bundled.key)?.markdown).toBe(bundled.markdown);
|
|
expect(await db.select().from(heartbeatRuns)).toHaveLength(0);
|
|
// Fail closed if a later actor changes either selection or source bytes.
|
|
const swapped = sources.map(s => ({ ...s, selected: !s.selected }));
|
|
await expect(verifyPlanSelection(api, companyId, agentId, swapped)).rejects.toThrow("selection differs");
|
|
await expect(verifyPlanSkills(api, companyId, [{ ...sources[0]!, sha256: "wrong" }])).rejects.toThrow("guidance changed");
|
|
}, 30_000);
|
|
});
|