mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-24 18:19:05 +02:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Agents need to discover connections, obtain consent, and continue from saved decisions. > - We want to reduce repeated instructions only when measured behavior supports the change. > - The existing decline tasks tell the model not to retry. One provider-decline check can pass without an explanation or an observed service counter. > - This PR adds neutral tasks and stricter saved-evidence checks before any connection instruction reduction. > - Production instructions remain unchanged. The new cells are configured, not live-qualified. ## Linked Issues or Issue Description Refs #15218. Refs #15389. **What existing behavior does this improve?** The Product E2E connection workflow evaluation and its instruction measurement provenance. **Current behavior** Some decline prompts supply the policy they intend to test. The provider-decline workflow does not require a saved post-decision explanation. Its old no-call check can use a missing fixture counter as zero. OpenCode has no connection cases in the original Everyday matrix. **Proposed behavior** Add an explicit-only suite with five connection stories on native Codex, ACPX Claude, and OpenCode. Require an explanation attributed by exact run ID after a saved decline. Observe the provider fixture counter. Preserve the original cases and grades. ## What Changed - Add fifteen configured cells with one attempt, twelve-minute deadlines, and verified 1,000-cent company and agent budget stops. - Remove procedure hints from the three new decline prompts. Keep a user-permitted explanation fallback and the existing positive controls. - Require saved decline state, one decision, unchanged connections, observed zero service calls where applicable, and a post-decision explanation from a successful run on the same task. - Add negative grader calibration and test the actual fixture budget payloads. Exclude the suite from default and generic selection. - Extend the existing full-catalog measurement source manifest with connection descriptions and schemas. Add an audit of fixed text, tool descriptions, returned instructions, and unqualified behavior. - Rebase on master `a6306ba606eb87c89b9ef0344e9fe8e0025580f9` and preserve its new Cursor suites. No production, credential, workflow, or lockfile change. ## Verification - Before rebase: Product E2E support passed 1,424 TypeScript tests and 128 Node checks. Six catalog measurement tests, repository typecheck/build, Product E2E typecheck, and exact fifteen-cell discovery passed. - The full pre-rebase repository test run was stopped when master advanced. Its partial result is not a pass. - After rebase and the review correction: repository build/typecheck, Product E2E typecheck, 1,799 TypeScript support tests (one skipped), 128 Node checks, six measurement tests, and exact fifteen-cell discovery pass. The duplicate local full-suite run was stopped incomplete after about 20 minutes once complete CI passed; no local full-suite pass is claimed. - Review found that the initial grader read `runId` instead of public `createdByRunId`. A regression calibration reproduced both rejection of valid public comments and acceptance of the wrong alias. The fix uses the actual field and binds the evidence type to the shared `IssueComment` contract. A subsequent type-only import path correction passes Product E2E typecheck. - Final source `0de306b9664bfbdebb6709ddb54c95152740d1ad` passes [complete CI](https://github.com/paperclipai/paperclip/actions/runs/37560250545): 51 successful checks and two intentional Storybook skips, plus separate Snyk success. Fresh Greptile review is 5/5 with the single review thread resolved and no new findings. The PR is clean and mergeable. - Local commands: `pnpm build`, `pnpm -r typecheck`, `pnpm test:e2e:runner:unit`, `pnpm test:e2e:runner:typecheck`, and `pnpm test:e2e:runner -- --list --suite native-connection-guidance`. The measurement uses `PAPERCLIP_NATIVE_PROCEDURE_MEASUREMENT=/tmp/connection-measurement.json pnpm exec vitest run --project @paperclipai/server server/src/__tests__/native-procedure-measurement.test.ts`. Validation used pinned pnpm 9.15.4. - No paid provider campaign was started. There is no baseline/candidate behavior result for these new cells. - The audit records 654 UTF-8 bytes of fixed connection guidance. A clean capture at `a04b8c6a452315625014888335d45670a2094fb6` confirms 41 supplied tools, 53,341 normalized bytes at start/resume, 50,949 at compact continuation, and a 48,195-byte authenticated OpenCode MCP catalog. These are byte counts, not tokens, bills, vendor-private prompt sizes, or savings from this PR. ## Risks - This is eval preparation. Passing support tests do not establish live model behavior or qualify an instruction reduction. - The explanation oracle checks attributed saved output. It does not prove cognition or arbitrary prose truthfulness. One saved interaction also does not prove the absence of repeated idempotent tool calls. - Successful new authentication and tool refresh, existing-connection agent grants, independent work while waiting, explicit retry after decline, and blocking when mandatory work remains still need separate coverage. - Notion setup decline does not execute a real Notion service. Positive service approval uses an already installed deterministic service; it does not qualify new connection creation. - The original historical failures remain unchanged. Future comparisons must freeze source, fixture, model, input, and grading controls and retain every actual attempt. ## Model Used OpenAI Codex, GPT-6 family, with reasoning, repository tools, and code execution. The exact serving model ID and context-window size were not exposed in this session; they are not inferred. No model provider was invoked by the eval suite in this PR preparation. ## 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 --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
91 lines
5.9 KiB
TypeScript
91 lines
5.9 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { runnerMatrix, runnerSuites } from "./catalog.js";
|
|
import { everydayTasks } from "./everyday-cases.js";
|
|
import { CONNECTION_GUIDANCE_SUITE, connectionGuidanceTasks, connectionGuidanceDefinitionDigest } from "./connection-guidance-cases.js";
|
|
import { gradeConnectionGuidanceDecline } from "./connection-guidance-evidence.js";
|
|
import { parseRunnerSelectors, selectRunnerExecutions } from "./selectors.js";
|
|
|
|
describe("neutral connection guidance selection", () => {
|
|
it("requires explicit selection and admits five stories on exactly three local native profiles", () => {
|
|
const selected = selectRunnerExecutions(parseRunnerSelectors(["--suite", CONNECTION_GUIDANCE_SUITE]));
|
|
expect(selected).toHaveLength(15);
|
|
expect(new Set(selected.map(row => row.profile.id))).toEqual(new Set(["runner-codex", "runner-acpx-claude", "runner-opencode"]));
|
|
for (const row of selected) {
|
|
expect(row.environment.id).toBe("local");
|
|
expect(row.task.automaticRetryPolicy).toBe("single_attempt");
|
|
expect(row.task.attemptTimeoutMs.local).toBe(720_000);
|
|
expect(row.task.expectedRunCount).toBe(2);
|
|
}
|
|
for (const args of [["--all"], ["--profile", "runner-opencode"]]) {
|
|
expect(selectRunnerExecutions(parseRunnerSelectors(args), runnerMatrix).some(row => row.suite.id === CONNECTION_GUIDANCE_SUITE)).toBe(false);
|
|
}
|
|
expect(runnerSuites.find(suite => suite.id === CONNECTION_GUIDANCE_SUITE)?.definitionMetadata)
|
|
.toMatchObject({ fixtureDigest: connectionGuidanceDefinitionDigest(), companyAndAgentBudgetCents: 1_000, maximumAttemptsPerCell: 1 });
|
|
});
|
|
it("keeps workflow instructions out of new decline prompts and preserves the historical prompts", () => {
|
|
for (const task of connectionGuidanceTasks) {
|
|
const original = everydayTasks.find(row => row.id === task.id)!;
|
|
if (task.id.endsWith("decline")) {
|
|
expect(task.buildPrompt("nonce")).toMatch(/brief explanation is enough/);
|
|
expect(task.buildPrompt("nonce")).not.toMatch(/declin|Not now|None for now|do not|retry|try again|connection_request|connections_search|paperclip_|yield|poll/i);
|
|
expect(task.buildPrompt("nonce")).not.toBe(original.buildPrompt("nonce"));
|
|
} else expect(task.buildPrompt("nonce")).toBe(original.buildPrompt("nonce"));
|
|
}
|
|
expect(everydayTasks.find(task => task.id === "service-decline")!.buildPrompt("nonce")).toContain("do not try again");
|
|
expect(everydayTasks.find(task => task.id === "connection-decline")!.buildPrompt("nonce")).toContain("do not try again");
|
|
});
|
|
});
|
|
|
|
const valid = {
|
|
caseId: "provider-decline" as const, decisionId: "decision", leadAgentId: "lead", issueId: "task",
|
|
decisions: [{ id: "decision", kind: "ask_user_questions", status: "answered", resolvedAt: "2026-10-06T22:00:00Z",
|
|
result: { answers: [{ questionId: "connection-provider:hubspot", optionIds: ["none"] }] } }],
|
|
replies: [{ authorAgentId: "lead", createdByRunId: "run", createdAt: "2026-10-06T22:00:02Z", body: "Contacts are unavailable after your decision." }],
|
|
runs: [{ id: "run", nativeIssueId: "task", agentId: "lead", status: "succeeded", finishedAt: "2026-10-06T22:00:03Z" }],
|
|
calls: 0, marker: "PRIVATE_MARKER", sameConnections: true,
|
|
};
|
|
const passes = (input: Parameters<typeof gradeConnectionGuidanceDecline>[0]) =>
|
|
gradeConnectionGuidanceDecline(input).every(check => check.passed);
|
|
|
|
describe("neutral decline evidence", () => {
|
|
it("accepts an attributed explanation after each kind of saved decline", () => {
|
|
expect(passes(valid)).toBe(true);
|
|
for (const [caseId, kind] of [["service-decline", "request_confirmation"], ["connection-decline", "connection_intent"]] as const) {
|
|
expect(passes({ ...valid, caseId, decisions: [{ ...valid.decisions[0], kind, status: "rejected" }] })).toBe(true);
|
|
}
|
|
});
|
|
it("rejects missing, stale, unattributed, wrong-agent, or unsuccessful explanations", () => {
|
|
for (const input of [
|
|
{ ...valid, replies: [] },
|
|
{ ...valid, replies: [{ ...valid.replies[0], body: "Done." }] },
|
|
{ ...valid, replies: [{ ...valid.replies[0], body: { text: "Unavailable" } }] },
|
|
{ ...valid, replies: [{ ...valid.replies[0], createdAt: "2026-10-06T21:59:00Z" }] },
|
|
{ ...valid, replies: [{ ...valid.replies[0], createdByRunId: undefined }] },
|
|
{ ...valid, replies: [{ ...valid.replies[0], createdByRunId: null, runId: "run" }] },
|
|
{ ...valid, replies: [{ ...valid.replies[0], createdByRunId: "other-run" }] },
|
|
{ ...valid, replies: [{ ...valid.replies[0], authorAgentId: "worker" }] },
|
|
{ ...valid, replies: [{ ...valid.replies[0], createdAt: "invalid" }] },
|
|
{ ...valid, runs: [{ ...valid.runs[0], agentId: "worker" }] },
|
|
{ ...valid, runs: [{ ...valid.runs[0], nativeIssueId: "other-task" }] },
|
|
{ ...valid, runs: [{ ...valid.runs[0], nativeIssueId: undefined }] },
|
|
{ ...valid, runs: [{ ...valid.runs[0], status: "failed" }] },
|
|
{ ...valid, runs: [{ ...valid.runs[0], finishedAt: "2026-10-06T21:59:00Z" }] },
|
|
]) expect(passes(input)).toBe(false);
|
|
});
|
|
it("rejects wrong or repeated decisions, early use, missing call evidence and connection changes", () => {
|
|
for (const input of [
|
|
{ ...valid, decisionId: "other" },
|
|
{ ...valid, decisions: [] },
|
|
{ ...valid, decisions: [...valid.decisions, ...valid.decisions] },
|
|
{ ...valid, decisions: [{ ...valid.decisions[0], resolvedAt: undefined }] },
|
|
{ ...valid, decisions: [{ ...valid.decisions[0], kind: "request_confirmation" }] },
|
|
{ ...valid, decisions: [{ ...valid.decisions[0], status: "pending" }] },
|
|
{ ...valid, decisions: [{ ...valid.decisions[0], result: { answers: [{ questionId: "connection-provider:hubspot", optionIds: ["via:arcade:hubspot"] }] } }] },
|
|
{ ...valid, calls: 1 },
|
|
{ ...valid, calls: undefined },
|
|
{ ...valid, sameConnections: false },
|
|
{ ...valid, replies: [{ ...valid.replies[0], body: "Unavailable, but PRIVATE_MARKER" }] },
|
|
]) expect(passes(input)).toBe(false);
|
|
});
|
|
});
|