mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-13 15:55:46 +01:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Agents read runtime prompts, tool help, skills, and role instructions. > - Several of these texts tell agents to create child tasks as a normal next step. > - One accepted-plan prompt even forbids work on the current task. > - Task parentage is useful, but it does not need a prompt that encourages task creation. > - This pull request removes those prompts and keeps the task APIs available. ## Linked Issues or Issue Description Refs #10642. This change adjusts prompt wording; it does not add delegation-cycle guards. **Current behavior** Prompts tell agents to create children after plan approval, use children for long work, and create a task just to pass context. Onboarding requires a child even for a single request. **Proposed behavior** Continue work on the current task. Keep explicit assignments, optional task creation, parentage, dependencies, and approval gates. Use the separate-task recipes when the user requests that structure or when continuing existing work. **Reason and benefit** Reduce needless task trees without removing subtask support. Keep the patch limited to wording, existing test expectations, and generated catalog data. ## What Changed - Align accepted-plan prompts and remove direct child-creation advice from runtime and error text. - Make task tool descriptions neutral. Keep their fields, defaults, and handlers. - Update the main skill, onboarding, CEO instructions, company templates, specialist workflows, and matching guides. - Keep historical release snapshots intact. Refresh the explicitly seeded schema fixture's catalog digest; the server still verifies historical release snapshot hashes. - Refresh the affected generated catalogs, capability inventory, semantic contracts, and fixture manifest. ## Verification - Final tested head: `c6e304d0c105cdbfbe0b2ab0a233307fcd91a04b`. - All 66 check runs are complete: 60 pass, one is neutral, and five are skipped by CI conditions. Snyk also passes. No checks fail or remain pending. - [PR CI](https://github.com/paperclipai/paperclip/actions/runs/42846226554) passes typecheck, build, server/general tests, all eight E2E shards, Runner support, Runner Rust, and the aggregate verify gate. [Windows](https://github.com/paperclipai/paperclip/actions/runs/42846226546) and [Docker Runner](https://github.com/paperclipai/paperclip/actions/runs/42846226108) also pass. - [Greptile](https://github.com/paperclipai/paperclip/pull/20362#issuecomment-6751419835) and [Paperclip review](https://github.com/paperclipai/paperclip/pull/20362#issuecomment-6751454010) give this exact head 5/5. All six inline threads are resolved. Neither current summary has findings. - Merged master `bf0220d34a156c81853755c3d1365ddc527037de`. Resolved three generated-file conflicts with the canonical generators. - Local verification passes: 473 focused prompt, continuation, UI, catalog, runtime and native-execution tests; Runner TypeScript build; generated metadata; UI token gates; and diff checks. - The prior head `c22018cb0f` failed three SDK evidence tests because two source hashes were stale. Reproduced all three failures locally. The existing generator refreshed only the hashes for the denial-copy file and its test. All 53 affected evidence/source-pin tests then passed, including private-value rejection. No scanner logic, assertions, or runtime dependency pins changed. Current CI confirms the support suite passes. - A prior commit failed the Rust process reaping test. Its cause remains unproven. No Rust code or assertions changed. Later Rust runs pass; this does not establish the cause of the earlier failure. - Local preflight built shared/plugin SDK packages, then stopped because `cargo` is absent. Current-head CI supplies Rust and full build coverage. `pnpm verify --explain` selects broad required CI; full local suites were not repeated. ## Risks - Wording changes guide the model; they do not enforce a ban on subtasks. - Existing installed skill versions and saved custom instructions need their normal refresh process. This PR does not migrate company configuration. - Existing approved task assignments and approval gates remain in effect. ## Model Used - OpenAI GPT-6, through Codex, with reasoning, tool use, and code execution. The exact routed model ID and context-window size were not exposed in 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 - [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>
185 lines
9.3 KiB
TypeScript
185 lines
9.3 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { readFile } from "node:fs/promises";
|
|
import { Ajv2020 } from "ajv/dist/2020.js";
|
|
import { CAPABILITY_SEMANTIC_TOOL_CATALOG } from "@paperclipai/paperclip-runner";
|
|
import {
|
|
ONBOARDING_FIRST_TASK_OPENING_INTERVIEW_OPTION_ID,
|
|
ONBOARDING_FIRST_TASK_OPENING_QUESTION_ID,
|
|
ONBOARDING_FIRST_TASK_OPENING_TASK_OPTION_ID,
|
|
buildOnboardingFirstTaskBrief,
|
|
buildOnboardingFirstTaskOpeningQuestion,
|
|
buildOnboardingFirstAgentInstructionsBundle,
|
|
fillFirstTaskPlaceholders,
|
|
renderChiefOfStaffPersona,
|
|
renderOnboardingFirstTaskGreeting,
|
|
} from "./onboarding-first-task-assets.js";
|
|
|
|
describe("fillFirstTaskPlaceholders", () => {
|
|
it("fills the name and organization when present", () => {
|
|
const out = fillFirstTaskPlaceholders(
|
|
"I'm {{agentName}}, chief of staff for {{organizationName}}.",
|
|
{ agentName: "Ada", organizationName: "Acme" },
|
|
);
|
|
expect(out).toBe("I'm Ada, chief of staff for Acme.");
|
|
});
|
|
|
|
it("drops the name and its trailing separator when no name is set", () => {
|
|
const out = fillFirstTaskPlaceholders("I'm {{agentName}}, your first agent teammate.", {
|
|
agentName: null,
|
|
});
|
|
expect(out).toBe("I'm your first agent teammate.");
|
|
});
|
|
|
|
it("falls back to a generic organization label when missing", () => {
|
|
const out = fillFirstTaskPlaceholders("for {{organizationName}}.", {});
|
|
expect(out).toBe("for your organization.");
|
|
});
|
|
});
|
|
|
|
describe("renderOnboardingFirstTaskGreeting", () => {
|
|
it("renders the board-approved greeting with the agent name", async () => {
|
|
const greeting = await renderOnboardingFirstTaskGreeting({ agentName: "Ada" });
|
|
expect(greeting).toContain("Welcome to Paperclip! I'm Ada, your first agent teammate.");
|
|
// The "what would you like to do" question moved onto the opening card.
|
|
expect(greeting).not.toContain("What would you like to do?");
|
|
});
|
|
});
|
|
|
|
describe("buildOnboardingFirstTaskOpeningQuestion", () => {
|
|
it("builds the two-option opening card with a free-text task option", async () => {
|
|
const payload = await buildOnboardingFirstTaskOpeningQuestion();
|
|
expect(payload.version).toBe(1);
|
|
expect(payload.supersedeOnUserComment).toBe(true);
|
|
expect(payload.submitLabel).toBe("Continue");
|
|
expect(payload.questions).toHaveLength(1);
|
|
const [question] = payload.questions;
|
|
expect(question.id).toBe(ONBOARDING_FIRST_TASK_OPENING_QUESTION_ID);
|
|
expect(question.selectionMode).toBe("single");
|
|
expect(question.required).toBe(true);
|
|
expect(question.prompt).toBe("What would you like to do?");
|
|
expect(question.options.map((option) => option.id)).toEqual([
|
|
ONBOARDING_FIRST_TASK_OPENING_INTERVIEW_OPTION_ID,
|
|
ONBOARDING_FIRST_TASK_OPENING_TASK_OPTION_ID,
|
|
]);
|
|
expect(question.options[0].label).toBe(
|
|
"Interview me and propose a plan and an agent team to execute it.",
|
|
);
|
|
expect(question.options[0].freeText).toBeUndefined();
|
|
expect(question.options[1].label).toBe("I have a task in mind");
|
|
expect(question.options[1].freeText).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("buildOnboardingFirstTaskBrief", () => {
|
|
it.each([
|
|
{ usePlanProposal: false, mode: "confirmation" },
|
|
{ usePlanProposal: true, mode: "plan" },
|
|
])("invokes the skill with $mode mode without inlining the policy", async ({ usePlanProposal, mode }) => {
|
|
const brief = await buildOnboardingFirstTaskBrief({ usePlanProposal });
|
|
expect(brief).toContain("Use the `first-task` skill (/first-task)");
|
|
expect(brief).toContain("Read its SKILL.md");
|
|
expect(brief).toContain("subsequent wakes of this task");
|
|
expect(brief).toContain(`Single-task proposal mode: \`${mode}\`.`);
|
|
expect(brief).not.toContain("{{");
|
|
expect(brief).not.toContain("Take the path the user picked.");
|
|
expect(brief).not.toContain("request_confirmation");
|
|
expect(brief).not.toContain("request_checkbox_confirmation");
|
|
});
|
|
});
|
|
|
|
describe("first-task proposal mode policy", () => {
|
|
it("maps both persisted brief modes to their proposal forms", async () => {
|
|
const skill = await readFile(new URL("../onboarding-assets/first-task/skills/first-task/SKILL.md", import.meta.url), "utf8");
|
|
expect(skill).toContain("`confirmation` means one `request_confirmation`");
|
|
expect(skill).toContain("`plan` means save a short `plan` document");
|
|
expect(skill).toContain("`request_checkbox_confirmation` targeting its saved revision");
|
|
expect(skill).toContain("explicit plan requests regardless of the single-task proposal mode");
|
|
});
|
|
|
|
it("hands accepted file-producing child work to its own execution before reporting results", async () => {
|
|
const skill = await readFile(new URL("../onboarding-assets/first-task/skills/first-task/SKILL.md", import.meta.url), "utf8");
|
|
expect(skill).toContain("If the accepted child work requires a file deliverable");
|
|
expect(skill).toContain("self-contained child description");
|
|
expect(skill).toContain("requested filenames");
|
|
expect(skill).toContain("child's own Paperclip execution");
|
|
expect(skill).toContain("even when the child is assigned to yourself");
|
|
expect(skill).toContain("Do not create or register that file from the onboarding run");
|
|
expect(skill).toContain("`register_deliverable` is bound to its current task");
|
|
expect(skill).toContain("preserve existing blocker IDs");
|
|
expect(skill).toContain("`set_dependencies`");
|
|
expect(skill).toContain("`paperclip_block`");
|
|
expect(skill).toContain("`blockedByIssueIds`");
|
|
expect(skill).toContain("End the turn so the child can execute");
|
|
expect(skill).toContain("Do not poll for its result");
|
|
expect(skill).toContain("verify the saved child document and retrievable file");
|
|
expect(skill).toContain("actual child task and download links");
|
|
});
|
|
|
|
it("ships native child handoff arguments that let the child run before its waiting parent", async () => {
|
|
const skill = await readFile(new URL("../onboarding-assets/first-task/skills/first-task/SKILL.md", import.meta.url), "utf8");
|
|
const recipe = [...skill.matchAll(/```json\s+([\s\S]*?)```/g)]
|
|
.map((match) => JSON.parse(match[1]!) as Record<string, Record<string, unknown>>)
|
|
.find((value) => value.createTask && value.setDependencies);
|
|
if (!recipe) throw new Error("Shipped first-task skill is missing the native child handoff recipe");
|
|
expect(Object.keys(recipe).sort()).toEqual(["createTask", "setDependencies"]);
|
|
expect(recipe.createTask).toEqual({
|
|
idempotencyKey: "first-task-child:<proposal-id>",
|
|
title: "<child-title>",
|
|
description: "<approved-child-description>",
|
|
assigneeActorId: "<self-agent-id>",
|
|
status: "todo",
|
|
blockedByTaskIds: [],
|
|
});
|
|
expect(recipe.setDependencies).toEqual({
|
|
idempotencyKey: "first-task-wait:<proposal-id>",
|
|
blockedByTaskIds: ["<existing-blocker-id>", "<child-task-id>"],
|
|
});
|
|
|
|
const ajv = new Ajv2020({ allErrors: true, allowUnionTypes: true, strict: true });
|
|
for (const [operationId, input] of [["create_task", recipe.createTask], ["set_dependencies", recipe.setDependencies]] as const) {
|
|
const descriptor = CAPABILITY_SEMANTIC_TOOL_CATALOG.find((tool) => tool.operationId === operationId);
|
|
if (!descriptor) throw new Error(`Missing native tool descriptor: ${operationId}`);
|
|
const validate = ajv.compile(descriptor.inputSchema);
|
|
expect(validate(input), JSON.stringify(validate.errors)).toBe(true);
|
|
expect(validate({ ...input, notAToolArgument: true })).toBe(false);
|
|
}
|
|
expect(skill).toContain("Copy required input payloads verbatim");
|
|
expect(skill).toContain("encoded bytes, nonces, constraints, and requested filenames");
|
|
expect(skill).toContain('Do not leave references such as "provided above"');
|
|
expect(skill).toContain("Parent-child linkage is not a dependency");
|
|
});
|
|
|
|
it("keeps plan-only authorization and document-only child execution distinct from file handoff", async () => {
|
|
const skill = await readFile(new URL("../onboarding-assets/first-task/skills/first-task/SKILL.md", import.meta.url), "utf8");
|
|
expect(skill).toContain("For a plan-only request, retain the accepted plan on this task");
|
|
expect(skill).toContain("Do not start its implementation or hire the proposed team without authorization");
|
|
expect(skill).toContain("For an accepted single task, complete the work here.");
|
|
expect(skill).toContain("For document-only child work, you may execute it directly");
|
|
expect(skill).toContain("Save the finished output as a document on the child task and mark that child done");
|
|
expect(skill).toContain("reuse any task already created for it");
|
|
});
|
|
});
|
|
|
|
describe("chief-of-staff persona", () => {
|
|
it("renders the persona with placeholders filled", async () => {
|
|
const persona = await renderChiefOfStaffPersona({
|
|
agentName: "Ada",
|
|
organizationName: "Acme",
|
|
});
|
|
expect(persona).toContain("You are Ada, chief of staff for Acme.");
|
|
expect(persona).not.toContain("{{agentName}}");
|
|
expect(persona).not.toContain("{{organizationName}}");
|
|
});
|
|
|
|
it("returns an AGENTS.md-keyed bundle for the first agent", async () => {
|
|
const bundle = await buildOnboardingFirstAgentInstructionsBundle({
|
|
agentName: "Ada",
|
|
organizationName: "Acme",
|
|
});
|
|
expect(bundle.entryFile).toBe("AGENTS.md");
|
|
expect(bundle.files).toEqual({
|
|
"AGENTS.md": await renderChiefOfStaffPersona({ agentName: "Ada", organizationName: "Acme" }),
|
|
});
|
|
});
|
|
});
|