Files
PaperClipAI/tests/runner-e2e/first-task-cases.ts
T
Devin FoleyandPaperclip 1aa0c36397 Fix execution after onboarding plan approval (#19698)
## Thinking Path

> - Paperclip manages agents and their work.
> - First-task onboarding turns a user's goal into an approved proposal.
> - The interview offers a plan and a team to carry out that plan.
> - The instructions leave execution after acceptance unclear.
> - An agent can add a plan-only approval boundary and finish without
doing the requested work.
> - This change makes the approval scope explicit and tests both
execution and plan-only outcomes.

## Linked Issues or Issue Description

**What happened?**

An onboarding agent can treat an interview about building a product as a
request for a plan only. It can then retain the approved plan and close
onboarding without creating execution tasks. The interview evaluation
previously accepted that result.

**Expected behavior**

Acceptance of an execution proposal starts its selected work. An
explicit plan-only request stops after planning. User-requested separate
approvals and existing permission and budget controls still apply.

**Steps to reproduce**

1. Choose the onboarding interview and describe a deliverable.
2. Accept the saved plan proposal.
3. Check that the agent starts the approved work instead of closing with
only a plan.

## What Changed

- Clarify execution intent, approval wording, continuation, and
completion in the first-task prompt.
- Require assigned child work and saved output in the interview
acceptance evaluation. Use checkbox acceptance in the regular interview
journey and retain conversational acceptance in the approval-reply
suite.
- Add an explicit plan-only acceptance journey and nine grader
regression checks.
- Update evaluation counts, grading versions, and onboarding
verification guidance.

## Verification

- Passed: all applicable CI checks on `198a6b47cac7`, including
typechecks, the general and serialized server tests, Runner checks,
build, and browser E2E shards. [CI
run](https://github.com/paperclipai/paperclip/actions/runs/42324855376).
- Passed locally: the full Runner support suite, 6,860 tests across 323
files (three skipped), plus 13 onboarding asset tests.
- Passed locally: the full workspace build, workspace package typechecks
(server rechecked after restoring excluded-plugin SDK links), and the
Runner evaluation TypeScript check.
- Passed: map validation and whitespace checks.
- Initial broad local checks failed during checkout preparation because
PostgreSQL library symlinks and runtime/SDK assets were missing.
Restoring the dependency setup made the affected controls pass. The full
root test suite passed in CI; it was not rerun locally after that
repair.
- Live model journeys have not been run. Grader tests do not prove model
compliance.

## Risks

- Prompt behavior needs live qualification across providers. This change
adds no server-side enforcement or permission bypass.
- The new plan-only case adds four local evaluation cells. A full
campaign now has 56 first-task cells.
- Existing proposals that reserve later approval keep that restriction.
This change does not rewrite tenant data or start existing onboarding
tasks.
- No schema migration or deployment is included.

## Model Used

- OpenAI `gpt-6.1-sol`, xhigh reasoning effort, with repository tools
and local test execution. Context window size is not exposed by this
session.

## 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 (focused and full Runner
support locally; full root coverage passed in CI)
- [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>
2026-11-06 17:56:27 -08:00

99 lines
4.9 KiB
TypeScript

import type { RunnerTaskFixture } from "./types.js";
import { APPROVAL_HISTORY_CASE, approvalHistoryFacts } from "./first-task-approval-history.js";
export const FIRST_TASK_CASES = [
["interview-first-response", "Interview: first response", "interview", 1],
["clear-task-first-response", "Clear task: first response", "task", 1],
[
"ambiguous-task-first-response",
"Ambiguous task: first response",
"ambiguous",
1,
],
[
"plain-message-first-response",
"Opening card replaced by a message",
"message",
1,
],
["plan-first-response", "Explicit plan: first response", "plan", 1],
[
"ordinary-task-control",
"Other tasks do not inherit onboarding policy",
"ordinary",
1,
],
["interview-plan-accept", "Interview, plan, and acceptance", "interview", 3],
["plan-only-accept", "Approve a plan without authorizing execution", "plan", 2],
["task-card-accept", "Subtask accepted through a card", "task", 3],
["accept-while-running", "Accept a proposal while its agent is still running", "task", 3],
["task-reply-accept", "Subtask accepted in conversation", "task", 3],
[
"clarify-propose-accept",
"Clarification, proposal, and acceptance",
"ambiguous",
4,
],
["revise-accept", "Revise scope before accepting", "task", 4],
["reject-no-execution", "Decline proposed work", "task", 2],
] as const;
const approvalHistoryCase = [APPROVAL_HISTORY_CASE, "Interview, plan scope, hiring, and execution approval", "interview", 7] as const;
export type FirstTaskCaseId = (typeof FIRST_TASK_CASES)[number][0] | typeof APPROVAL_HISTORY_CASE;
export type FirstTaskOpening = (typeof FIRST_TASK_CASES)[number][2];
export const firstTaskMarker = (nonce: string) =>
`GARDEN${nonce.replace(/[^a-z0-9]/gi, "")}`;
export function firstTaskScenario(id: string, nonce: string) {
const entry = FIRST_TASK_CASES.find((row) => row[0] === id) ?? (id === APPROVAL_HISTORY_CASE ? approvalHistoryCase : undefined);
if (!entry) throw new Error(`Unknown first-task case ${id}`);
const marker = firstTaskMarker(nonce);
const originalMarker = id === "revise-accept" ? `DRAFT${marker}` : marker;
const task = `I need a two-sentence welcome note for our neighborhood garden club. It should invite beginners to our free Saturday meetup and include the phrase ${originalMarker}. Save the finished note as a document attached to the task.`;
const facts = `We run a neighborhood garden club for beginners. Our first goal is a two-sentence welcome note inviting people to our free Saturday meetup. We have no budget and need no outside tools. Done means the note is saved as a task document and includes ${marker}.`;
return {
id: entry[0],
opening: entry[2],
marker,
originalMarker,
facts: id === APPROVAL_HISTORY_CASE ? approvalHistoryFacts(marker) : facts,
firstResponseOnly:
id.endsWith("first-response") || id === "ordinary-task-control",
prompt:
entry[2] === "ambiguous"
? "Help me get my organization going."
: entry[2] === "plan"
? `Write a short plan for welcoming new members to our garden club. ${facts} ${id === "plan-only-accept"
? "For this task, deliver only a plan. Do not implement it, hire agents, or create execution tasks even after I approve the plan."
: "Let me review the plan before any work starts."}`
: entry[2] === "ordinary"
? `Write the welcome note now on this task; no interview or subtask is needed. ${task}`
: task,
revision: `Change the note: invite people to our Sunday meetup instead, and replace ${originalMarker} with ${marker}. Please revise your proposal; I have not accepted it yet.`,
acceptance: id === "plan-only-accept"
? "I approve the plan only. Do not start implementation, hire agents, or create execution tasks."
: "Yes, I accept that proposal. Please do it.",
rejection:
"No, do not proceed. I do not want this work done. Please acknowledge that and stop.",
};
}
const buildFirstTaskFixture = ([id, label, , expectedRunCount]: readonly [string, string, string, number]): RunnerTaskFixture => ({
id,
label,
groups: ["onboarding"],
flow: "first_task",
workMode: "standard",
expectedRunCount,
attemptTimeoutMs: { local: 15 * 60_000, daytona: 15 * 60_000 },
expectedTerminalState: { issue: "in_review", run: "succeeded" },
buildTitle: (nonce) => `First task ${id} ${nonce}`,
buildPrompt: (nonce) => firstTaskScenario(id, nonce).prompt,
buildVisibleMarker: firstTaskMarker,
buildMatchers: () => [], // Checkpoints and state effects are graded by first-task-scoring.
});
export const firstTaskTasks: readonly RunnerTaskFixture[] = FIRST_TASK_CASES.map(buildFirstTaskFixture);
export const onboardingApprovalHistoryTask = {
...buildFirstTaskFixture(approvalHistoryCase),
expectedTerminalState: { issue: "done", run: "succeeded" } as const,
minimumExpectedRunCount: 1,
automaticRetryPolicy: "single_attempt" as const,
};