mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-21 16:27:18 +02:00
Calibrate planning task creation against the current composer
Retain the zero-model UI setup failure and create tasks through the real composer response. Remove random chat-fixture prefix collisions. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
370e51d110
commit
8538cfce4c
7 files changed
+115
-7
No files matched your search
@@ -65,3 +65,30 @@ of a bare issue-ID URL without the issues route. Product E2E typecheck passes.
|
||||
Initial local DB checks were skipped until the dependency's missing library
|
||||
symlinks were restored with its supplied postinstall script; skipped checks were
|
||||
not counted as passes.
|
||||
|
||||
The next setup campaign, [37401094799](https://github.com/paperclipai/paperclip/actions/runs/37401094799),
|
||||
measured source `370e51d110836b942e5f90567d2bbe260bcc0f3a` using trusted workflow
|
||||
revision `0e0b63e5a551388ac4601ed982b3e8f1c772f123` (workflow blob
|
||||
`0600886144d3e22ea2e4a38329a79177882f3948`). It remains an original FAIL,
|
||||
classified by the harness as `candidate_failure`. Inspection shows a browser
|
||||
fixture error before task creation: the shared helper waited for the old Task
|
||||
title field. `runIds`, the company run ledger, and planning observation are all
|
||||
empty; no model executed. Cleanup passed. The billing summary's runCount=1 is
|
||||
its minimum-one placeholder (`billing.ts`), not evidence of a provider run;
|
||||
runtime/actual charges remain unmetered. No original result is regraded.
|
||||
|
||||
The corrected planning-only browser helper uses the current description composer,
|
||||
explicitly selects the owner, and captures the public task-create response ID. A
|
||||
real browser/server/database calibration creates the exact prompt/assignment with
|
||||
paused non-provider agents, confirms zero run rows, and deletes its company. It
|
||||
passes. Before any model execution, prompts also explicitly name the already
|
||||
required result document key and exact JSON fields, avoiding an unstated oracle
|
||||
format assumption. The legacy helper and production UI remain unchanged.
|
||||
|
||||
Repository checks at the previous source: full typecheck and build pass. Local
|
||||
full tests: 9,591 pass, 5,796 skip and one unchanged macOS real-Git streaming test
|
||||
hits its five-minute deadline; an isolated check also fails. Preserve this
|
||||
limitation. Normal CI found a different, exact cause: truncated random fixture
|
||||
company prefixes collided in chat integration setup. A monotonic per-suite
|
||||
prefix removes that collision; all five affected native-modal variants pass
|
||||
locally (1,058 unrelated cases filtered). This is a test-only repair.
|
||||
@@ -1080,6 +1080,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => {
|
||||
.where(and(inArray(chatConversations.companyId, companyIds), inArray(chatConversations.state, ["active", "waiting"])));
|
||||
}
|
||||
|
||||
let fixtureCompanySequence = 0;
|
||||
async function seedCompany() {
|
||||
const companyId = randomUUID();
|
||||
fixtureCompanies.add(companyId);
|
||||
@@ -1088,7 +1089,7 @@ describeEmbeddedPostgres("chat channel control-plane integration", () => {
|
||||
await db.insert(companies).values({
|
||||
id: companyId,
|
||||
name: `Chat Test ${companyId.slice(0, 8)}`,
|
||||
issuePrefix: `C${companyId.replaceAll("-", "").slice(0, 7).toUpperCase()}`,
|
||||
issuePrefix: `C${(++fixtureCompanySequence).toString(36).padStart(7, "0").toUpperCase()}`,
|
||||
requireBoardApprovalForNewAgents: false,
|
||||
});
|
||||
const now = new Date();
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { createPlanTaskThroughUi } from "../runner-e2e/plan-task-ui.js";
|
||||
|
||||
test("planning fixture creates the exact assignment through the current composer without providers", async ({ page, request }) => {
|
||||
const companyResponse = await request.post("/api/companies", { data: { name: `Planning setup ${randomUUID()}` } });
|
||||
expect(companyResponse.ok()).toBe(true);
|
||||
const company = await companyResponse.json();
|
||||
try {
|
||||
let lead: { id: string; name: string } | undefined;
|
||||
for (const name of ["A Different Owner", "Planning Fixture Lead"]) {
|
||||
const response = await request.post(`/api/companies/${company.id}/agents`, { data: {
|
||||
name, role: "general", adapterType: "process", adapterConfig: { command: "false" },
|
||||
runtimeConfig: { heartbeat: { enabled: false } },
|
||||
} });
|
||||
expect(response.ok()).toBe(true);
|
||||
const agent = await response.json();
|
||||
expect((await request.post(`/api/agents/${agent.id}/pause`)).ok()).toBe(true);
|
||||
lead = agent;
|
||||
}
|
||||
const prompt = "Plan the work briefly, then carry it out. Verify a sample order and save the requested result.";
|
||||
const created = await createPlanTaskThroughUi({ page, companyId: company.id, issuePrefix: company.issuePrefix,
|
||||
agentName: lead!.name, prompt });
|
||||
const issue = await (await request.get(`/api/issues/${created.id}`)).json();
|
||||
expect(issue).toMatchObject({ companyId: company.id, assigneeAgentId: lead!.id, description: prompt, workMode: "standard" });
|
||||
const runs = await request.get(`/api/companies/${company.id}/heartbeat-runs?limit=100`);
|
||||
expect(runs.ok()).toBe(true);
|
||||
expect(await runs.json()).toEqual([]);
|
||||
} finally {
|
||||
expect((await request.delete(`/api/companies/${company.id}`)).ok()).toBe(true);
|
||||
}
|
||||
});
|
||||
@@ -54,3 +54,22 @@ on source `843238f43cb266f6ca0bc9d255881558ffd2eb71` was cancelled after source
|
||||
review identified unsupported bundled edits and automatic core reinstallation.
|
||||
The paid-cell step was skipped: zero provider runs and no behavioral grade.
|
||||
The corrected fixture uses company copies and selection only.
|
||||
|
||||
The next setup campaign, [37401094799](https://github.com/paperclipai/paperclip/actions/runs/37401094799),
|
||||
measured source `370e51d110836b942e5f90567d2bbe260bcc0f3a` using trusted workflow
|
||||
revision `0e0b63e5a551388ac4601ed982b3e8f1c772f123` (workflow blob
|
||||
`0600886144d3e22ea2e4a38329a79177882f3948`). It remains an original FAIL,
|
||||
classified by the harness as `candidate_failure`. Inspection shows a browser
|
||||
fixture error before task creation: the shared helper waited for the old Task
|
||||
title field. `runIds`, the company run ledger, and planning observation are all
|
||||
empty; no model executed. Cleanup passed. The billing summary's runCount=1 is
|
||||
its minimum-one placeholder (`billing.ts`), not evidence of a provider run;
|
||||
runtime/actual charges remain unmetered. No original result is regraded.
|
||||
|
||||
The corrected planning-only browser helper uses the current description composer,
|
||||
explicitly selects the owner, and captures the public task-create response ID. A
|
||||
real browser/server/database calibration creates the exact prompt/assignment with
|
||||
paused non-provider agents, confirms zero run rows, and deletes its company. It
|
||||
passes. Before any model execution, prompts also explicitly name the already
|
||||
required result document key and exact JSON fields, avoiding an unstated oracle
|
||||
format assumption. The legacy helper and production UI remain unchanged.
|
||||
@@ -15,7 +15,7 @@ export const PLAN_SKILLS = [
|
||||
] as const;
|
||||
export const planHash = (value: string | Buffer) => createHash("sha256").update(value).digest("hex");
|
||||
export const planDefinitionDigest = planHash([
|
||||
"plan-task-cases.ts", "plan-task-flow.ts", "plan-task-scoring.ts", "plan-task-skills.ts",
|
||||
"plan-task-cases.ts", "plan-task-flow.ts", "plan-task-scoring.ts", "plan-task-skills.ts", "plan-task-ui.ts",
|
||||
"fixtures/plan-task-guidance/current-conversion.md", "fixtures/plan-task-guidance/current-planning.md",
|
||||
].map(file => readFileSync(new URL(file, import.meta.url), "utf8")).join("\0"));
|
||||
|
||||
@@ -33,7 +33,7 @@ export function planScenario(caseId: PlanCase, nonce: string) {
|
||||
dependency: `Prepare the release receipt. Alex Metrics owns verifying the order: two units at 7 dollars plus three at 5 dollars. Alex must save a result document with fields marker "${marker}", units (the summed quantity), and total (the calculated dollar amount) on Alex's work item. Riley Copy owns release verification and must inspect that actual saved result after Alex completes it; Riley cannot perform release work before that prerequisite is complete. Riley's result document must contain fields marker "${marker}", releasedTotal (the verified dollar total), and sourceRevisionId (the actual revision ID of Alex's result). Deliver Riley's verified receipt as this task's result document and finish.`,
|
||||
review: `Obtain Riley Copy's independent audit of this supplied order proposal: two units at 7 dollars, three units at 5 dollars, proposed total 30 dollars. Riley must author a result document on Riley's review work item with fields marker "${marker}", verdict ("accept" or "reject"), correctTotal (the independently calculated dollar total), and difference (the absolute error in the proposal). The audit, including any adverse finding, is the complete requested deliverable; do not implement a correction. Report Riley's actual verdict in this task's result document with those same fields and finish.`,
|
||||
};
|
||||
return { marker, prompt: `Plan the work briefly, then carry it out. Execution is already authorized; no additional approval is requested. Choose how to organize the work. ${prompts[caseId]} Documents may use a JSON code block. Link the final saved result in your response.` };
|
||||
return { marker, prompt: `Plan the work briefly, then carry it out. Execution is already authorized; no additional approval is requested. Choose how to organize the work. ${prompts[caseId]} Save every requested result document with key result and exactly the requested JSON fields. Documents may use a JSON code block. Link the final saved result in your response.` };
|
||||
}
|
||||
|
||||
export function planTaskProfile(profile: RunnerProfileFixture): RunnerProfileFixture {
|
||||
|
||||
@@ -6,7 +6,7 @@ import { collectChatRunEvidence } from "./chat-flow.js";
|
||||
import { FixtureRegistry } from "./fixture-registry.js";
|
||||
import type { LiveFixtureValues } from "./live-fixtures.js";
|
||||
import type { MatrixExecution } from "./types.js";
|
||||
import { createTaskThroughUi } from "./user-actions.js";
|
||||
import { createPlanTaskThroughUi } from "./plan-task-ui.js";
|
||||
import { PLAN_BASE_SHA, PLAN_BUDGET_CENTS, PLAN_MAX_RUNS, parsePlanCase, planDefinitionDigest, planScenario } from "./plan-task-cases.js";
|
||||
import { gradePlanTask, type PlanCheck, type PlanDocument, type PlanObservation, type PlanRow } from "./plan-task-scoring.js";
|
||||
import { preparePlanSkills, selectPlanSkills, verifyPlanSelection, verifyPlanSkills } from "./plan-task-skills.js";
|
||||
@@ -82,10 +82,10 @@ export async function runPlanTaskFlow(input: Input) {
|
||||
}
|
||||
await input.evidence("plan-task-source.json", source);
|
||||
await api.patch("/api/instance/settings/experimental", { enableClassicTaskInterface: false });
|
||||
await createTaskThroughUi({ page, issuePrefix: f.company.issuePrefix!, agentName: f.agent.name,
|
||||
title: execution.task.buildTitle(input.nonce), prompt: scenario.prompt, workMode: "standard" });
|
||||
const created = await createPlanTaskThroughUi({ page, companyId: f.company.id, issuePrefix: f.company.issuePrefix!,
|
||||
agentName: f.agent.name, prompt: scenario.prompt });
|
||||
parent = await pollUntil({ label: "browser-created planning-guidance task", deadlineAt: input.deadlineAt,
|
||||
load: async () => (await api.get<PlanRow[]>(`${company}/issues`)).find(i => i.title === execution.task.buildTitle(input.nonce)), accept: Boolean });
|
||||
load: () => api.get<PlanRow>(`/api/issues/${created.id}`), accept: i => i.id === created.id });
|
||||
if (!parent) throw new Error("Browser task not found");
|
||||
let previous = "", same = 0;
|
||||
await pollUntil({ label: "planning work and handoffs settle", deadlineAt: input.deadlineAt, intervalMs: 1_500, load: observe,
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { expect, type Page } from "@playwright/test";
|
||||
|
||||
/** The current composer creates a task from its description and assigns its
|
||||
* title later. Capture the actual public create response; never guess its ID
|
||||
* from a title or mutate the issue behind the browser. */
|
||||
export async function createPlanTaskThroughUi(input: {
|
||||
page: Page; companyId: string; issuePrefix: string; agentName: string; prompt: string;
|
||||
}): Promise<{ id: string }> {
|
||||
const { page } = input;
|
||||
await page.goto(`/${encodeURIComponent(input.issuePrefix)}/issues`, { waitUntil: "domcontentloaded" });
|
||||
await page.getByRole("button", { name: "New Task", exact: true }).first().click();
|
||||
const dialog = page.getByRole("dialog").filter({ has: page.getByRole("heading", { name: "New task", exact: true }) });
|
||||
await dialog.getByRole("textbox", { name: "editable markdown", exact: true }).fill(input.prompt);
|
||||
const assignee = dialog.getByRole("button", { name: "Select assignee", exact: true });
|
||||
await assignee.click();
|
||||
await page.getByRole("searchbox", { name: "Search assignees", exact: true }).fill(input.agentName);
|
||||
const option = page.getByRole("listbox", { name: "Assignees" }).getByRole("option").filter({ has: page.getByText(input.agentName, { exact: true }) });
|
||||
await expect(option).toHaveCount(1);
|
||||
await option.click();
|
||||
await expect(assignee).toContainText(input.agentName);
|
||||
const responsePromise = page.waitForResponse(response => response.request().method() === "POST" &&
|
||||
new URL(response.url()).pathname === `/api/companies/${input.companyId}/issues`);
|
||||
await dialog.getByRole("button", { name: "Create task", exact: true }).click();
|
||||
const response = await responsePromise;
|
||||
if (!response.ok()) throw new Error(`Browser task creation failed: ${response.status()}`);
|
||||
const issue = await response.json();
|
||||
if (typeof issue.id !== "string" || issue.companyId !== input.companyId) throw new Error("Browser created an unexpected issue identity");
|
||||
return { id: issue.id };
|
||||
}
|
||||
Reference in new issue
Block a user