mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-14 08:53:19 +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>
184 lines
7.6 KiB
TypeScript
184 lines
7.6 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import {
|
|
ISSUE_WRITE_DENIAL_CODES,
|
|
describeIssueWriteDenial,
|
|
isIssueWriteDenialCode,
|
|
issueWriteDenialApiMessage,
|
|
issueWriteDenialCodeForResponsibleUserDenial,
|
|
issueWriteDenialResponse,
|
|
} from "./issue-write-denial.js";
|
|
|
|
describe("describeIssueWriteDenial", () => {
|
|
it("answers all three plan §6 obligations for every code", () => {
|
|
for (const code of ISSUE_WRITE_DENIAL_CODES) {
|
|
const copy = describeIssueWriteDenial(code);
|
|
expect(copy.code, code).toBe(code);
|
|
// Boundary that fired, who can act, sanctioned path — none may be empty.
|
|
expect(copy.boundary.trim().length, code).toBeGreaterThan(0);
|
|
expect(copy.whoCanAct.trim().length, code).toBeGreaterThan(0);
|
|
expect(copy.sanctionedPath.trim().length, code).toBeGreaterThan(0);
|
|
expect(copy.title.trim().length, code).toBeGreaterThan(0);
|
|
expect(copy.description.trim().length, code).toBeGreaterThan(0);
|
|
}
|
|
});
|
|
|
|
it("never leaks a raw id or placeholder when labels are unknown", () => {
|
|
for (const code of ISSUE_WRITE_DENIAL_CODES) {
|
|
const copy = describeIssueWriteDenial(code);
|
|
const prose = `${copy.description} ${copy.whoCanAct} ${copy.sanctionedPath}`;
|
|
expect(prose, code).not.toMatch(/undefined|null/);
|
|
// Unknown labels degrade to generic nouns, never a bare id.
|
|
expect(prose, code).toMatch(/this task|this agent|the current assignee|the responsible user/);
|
|
}
|
|
});
|
|
|
|
it("keeps the boundary free of parentheses and distinct from the title", () => {
|
|
for (const code of ISSUE_WRITE_DENIAL_CODES) {
|
|
const copy = describeIssueWriteDenial(code, { cap: 20 });
|
|
// Surfaces render the boundary inside their own parens — nesting stutters.
|
|
expect(copy.boundary, code).not.toMatch(/[()]/);
|
|
// A title echoed verbatim as its own boundary reads as a mistake.
|
|
expect(copy.boundary.toLowerCase(), code).not.toBe(copy.title.toLowerCase());
|
|
}
|
|
});
|
|
|
|
it("names the actor, assignee, and task when they are known", () => {
|
|
const copy = describeIssueWriteDenial("issue_write_not_visible", {
|
|
actorLabel: "Fable",
|
|
assigneeLabel: "CodexCoder",
|
|
issueIdentifier: "TASK-482",
|
|
});
|
|
expect(copy.description).toContain("TASK-482");
|
|
expect(copy.description).toContain("Fable");
|
|
expect(copy.whoCanAct).toContain("CodexCoder");
|
|
});
|
|
|
|
it("points a visibility denial at the missing permission", () => {
|
|
const copy = describeIssueWriteDenial("issue_write_not_visible");
|
|
expect(copy.sanctionedPath).toContain("Ask the board to widen visibility");
|
|
});
|
|
|
|
it("frames the per-run cap as a rate backstop, not a permission decision", () => {
|
|
const copy = describeIssueWriteDenial("cross_issue_influence_cap_exceeded", {
|
|
cap: 20,
|
|
count: 21,
|
|
actorLabel: "Fable",
|
|
});
|
|
expect(copy.status).toBe(429);
|
|
expect(copy.tone).toBe("cap");
|
|
expect(copy.boundary).toContain("20");
|
|
expect(copy.description).toContain("attempt 21");
|
|
expect(copy.description).toContain("still allowed");
|
|
expect(copy.sanctionedPath).toContain("next heartbeat");
|
|
});
|
|
|
|
it("defaults the cap to the shipped limit when context omits it", () => {
|
|
const copy = describeIssueWriteDenial("cross_issue_influence_cap_exceeded");
|
|
expect(copy.boundary).toContain("20");
|
|
expect(copy.description).not.toContain("attempt");
|
|
});
|
|
|
|
it("gives the run-context denial a copy-pasteable fix", () => {
|
|
const copy = describeIssueWriteDenial("cross_issue_influence_run_context_required");
|
|
expect(copy.sanctionedPath).toContain("X-Paperclip-Run-Id");
|
|
expect(copy.sanctionedPath).toContain("PAPERCLIP_RUN_ID");
|
|
});
|
|
|
|
it("tells a spoof attempt that the write itself was fine", () => {
|
|
const copy = describeIssueWriteDenial("issue_write_attribution_spoof_rejected", {
|
|
actorLabel: "Fable",
|
|
responsibleUserName: "Dotta",
|
|
});
|
|
expect(copy.status).toBe(422);
|
|
expect(copy.whoCanAct).toContain("Fable");
|
|
expect(copy.sanctionedPath).toContain("Dotta");
|
|
expect(copy.sanctionedPath).toContain("onBehalfOfUserId");
|
|
});
|
|
|
|
it("routes a run lock to comments, which stay open", () => {
|
|
const copy = describeIssueWriteDenial("issue_write_assignee_run_lock", {
|
|
assigneeLabel: "CodexCoder",
|
|
});
|
|
expect(copy.status).toBe(409);
|
|
expect(copy.tone).toBe("lock");
|
|
expect(copy.sanctionedPath).toContain("Comment instead");
|
|
expect(copy.sanctionedPath).toContain("CodexCoder");
|
|
});
|
|
|
|
it("reuses responsible-user ceiling copy and keeps on-behalf-of terminology", () => {
|
|
const ceiling = describeIssueWriteDenial("issue_write_responsible_user_ceiling", {
|
|
responsibleUserName: "Dotta",
|
|
issueIdentifier: "TASK-517",
|
|
});
|
|
expect(ceiling.title).toBe("Responsible user not authorized");
|
|
expect(ceiling.description).toContain("on behalf");
|
|
expect(ceiling.description).toContain("TASK-517");
|
|
expect(ceiling.description).not.toContain("impersonat");
|
|
expect(ceiling.whoCanAct).toContain("Dotta");
|
|
|
|
const unavailable = describeIssueWriteDenial("issue_write_responsible_user_unavailable", {
|
|
responsibleUserName: "Dotta",
|
|
});
|
|
expect(unavailable.title).toBe("Responsible user unavailable");
|
|
expect(unavailable.sanctionedPath).toContain("blocked");
|
|
});
|
|
|
|
it("falls back to generic phrasing when the responsible user is unknown", () => {
|
|
const copy = describeIssueWriteDenial("issue_write_responsible_user_ceiling");
|
|
expect(copy.description).toContain("the responsible user");
|
|
});
|
|
});
|
|
|
|
describe("issueWriteDenialCodeForResponsibleUserDenial", () => {
|
|
it("bridges both authorization-layer ceiling codes", () => {
|
|
expect(issueWriteDenialCodeForResponsibleUserDenial("RESPONSIBLE_USER_UNAUTHORIZED"))
|
|
.toBe("issue_write_responsible_user_ceiling");
|
|
expect(issueWriteDenialCodeForResponsibleUserDenial("RESPONSIBLE_USER_UNAVAILABLE"))
|
|
.toBe("issue_write_responsible_user_unavailable");
|
|
});
|
|
});
|
|
|
|
describe("isIssueWriteDenialCode", () => {
|
|
it("accepts shipped codes and rejects everything else", () => {
|
|
expect(isIssueWriteDenialCode("issue_write_not_visible")).toBe(true);
|
|
expect(isIssueWriteDenialCode("RESPONSIBLE_USER_UNAUTHORIZED")).toBe(false);
|
|
expect(isIssueWriteDenialCode(null)).toBe(false);
|
|
expect(isIssueWriteDenialCode(undefined)).toBe(false);
|
|
expect(isIssueWriteDenialCode("")).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("issueWriteDenialApiMessage", () => {
|
|
it("keeps boundary, who-can-act, and sanctioned path in the flattened error", () => {
|
|
const copy = describeIssueWriteDenial("issue_write_not_visible", {
|
|
actorLabel: "Fable",
|
|
issueIdentifier: "TASK-482",
|
|
});
|
|
const message = issueWriteDenialApiMessage(copy);
|
|
expect(message).toContain(copy.boundary);
|
|
expect(message).toContain("Who can act:");
|
|
expect(message).toContain("Try this:");
|
|
expect(message).toContain(copy.sanctionedPath);
|
|
});
|
|
});
|
|
|
|
describe("issueWriteDenialResponse", () => {
|
|
it("pairs the status with a machine-readable details payload", () => {
|
|
const { status, body } = issueWriteDenialResponse("cross_issue_influence_cap_exceeded", {
|
|
cap: 20,
|
|
count: 21,
|
|
});
|
|
expect(status).toBe(429);
|
|
expect(body.details.code).toBe("cross_issue_influence_cap_exceeded");
|
|
expect(body.details.boundary).toContain("20");
|
|
expect(body.error).toContain("Who can act:");
|
|
});
|
|
|
|
it("uses the status each code declares", () => {
|
|
expect(issueWriteDenialResponse("issue_write_assignee_run_lock").status).toBe(409);
|
|
expect(issueWriteDenialResponse("issue_write_attribution_spoof_rejected").status).toBe(422);
|
|
expect(issueWriteDenialResponse("issue_write_not_visible").status).toBe(403);
|
|
});
|
|
});
|