fix(agents): reduce default manual and shared legacy guidance

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-02 11:25:09 -05:00
1 parent cf8ad63c80
commit 19d685a20c
13 files changed
+89 -228

No files matched your search

@@ -596,6 +596,9 @@ describe("shared ACPX engine runtime behavior", () => {
const fresh = await runExecutor(config, { context });
expect(fresh.turnInputs).toHaveLength(1);
const prompt = String(fresh.turnInputs[0]?.text ?? "");
expect(prompt).toContain("You are agent agent-1");
expect(prompt).toContain("Connection tools:");
expect(prompt).not.toContain("Execution contract:");
expect(prompt).toContain(context.paperclipTaskMarkdownAssignment);
expect(prompt).toContain(context.paperclipTaskCommunicationGuidance);
expect(prompt).not.toContain('"objective":');
@@ -609,10 +612,18 @@ describe("shared ACPX engine runtime behavior", () => {
const resumed = await runExecutor(config, { context, runtime: { sessionParams: fresh.result.sessionParams } });
expect(resumed.sessionInputs[0]?.resumeSessionId).toBe(fresh.result.sessionId);
const resumedPrompt = String(resumed.turnInputs[0]?.text ?? "");
expect(resumedPrompt).not.toContain("Execution contract:");
expect(resumedPrompt).toContain(context.paperclipTaskMarkdownAssignmentCompact);
expect(resumedPrompt).not.toContain(context.paperclipTaskCommunicationGuidance);
expect(resumedPrompt).not.toContain('"id":"comment-first"');
expect(resumedPrompt).toContain('"id":"comment-second"');
const reset = await runExecutor(config, { context });
expect(reset.sessionInputs[0]?.resumeSessionId).toBeUndefined();
const resetPrompt = String(reset.turnInputs[0]?.text ?? "");
expect(resetPrompt).toContain("You are agent agent-1");
expect(resetPrompt).toContain("Connection tools:");
expect(resetPrompt).toContain(context.paperclipTaskMarkdownAssignment);
expect(resetPrompt).not.toContain("Execution contract:");
});
it("includes Paperclip env and API access notes in the ACPX prompt without leaking the token", async () => {
@@ -746,10 +757,10 @@ describe("shared ACPX engine runtime behavior", () => {
expect(prompt).not.toContain("Create child issues");
expect(prompt).not.toContain("Use child issues");
}
expect(String(fresh.meta[0]?.prompt)).toContain(custom ? "Custom agent instructions." : "Continue your Paperclip conversation");
expect(String(reset.meta[0]?.prompt)).toContain(custom ? "Custom agent instructions." : "Continue your Paperclip conversation");
expect(String(fresh.meta[0]?.prompt)).toContain(custom ? "Custom agent instructions." : "You are agent agent-1");
expect(String(reset.meta[0]?.prompt)).toContain(custom ? "Custom agent instructions." : "You are agent agent-1");
const ordinary = await runExecutor({ ...config, promptTemplate: "" }, { context: { ...context, conversationMode: false } });
expect(String(ordinary.meta[0]?.prompt)).toContain("Execution contract:");
expect(String(ordinary.meta[0]?.prompt)).not.toContain("Execution contract:");
expect(String(ordinary.meta[0]?.prompt)).toContain("Create child issues from the approved plan");
});
@@ -3,13 +3,14 @@ import { selectPaperclipPromptSections as selectSections } from "./server-utils.
import { createPromptContextFixture } from "./test-fixtures/prompt-context.js";
describe("task and event section ownership", () => {
it("lets a separate instruction carrier own the execution contract on a resumed turn", () => {
it("preserves resumed wake data without reintroducing generic procedures", () => {
const context = createPromptContextFixture();
const sections = selectSections(context, { resumedSession: true, includeExecutionContract: false });
expect(sections.taskContextNote).toBe(context.paperclipTaskMarkdownAssignmentCompact);
expect(sections.wakePrompt).toContain('"id":"comment-second"');
expect(sections.wakePrompt).not.toContain("Execution contract:");
expect(selectSections(context, { resumedSession: true }).wakePrompt).toContain("Execution contract:");
for (const includeExecutionContract of [undefined, false, true]) {
const sections = selectSections(context, { resumedSession: true, includeExecutionContract });
expect(sections.taskContextNote).toBe(context.paperclipTaskMarkdownAssignmentCompact);
expect(sections.wakePrompt).toContain('"id":"comment-second"');
expect(sections.wakePrompt).not.toContain("Execution contract:");
}
});
it("preserves user repetition and distinct same-body comments under their source owners", () => {
+30 -104
View File
@@ -924,7 +924,7 @@ describe("renderPaperclipWakePrompt", () => {
fallbackFetchNeeded: false,
};
const ordinary = renderPaperclipWakePrompt(payload, { resumedSession: true });
expect(ordinary).toContain("Execution contract:");
expect(ordinary).not.toContain("Execution contract:");
expect(ordinary).toContain("Create child issues from the approved plan");
for (const resumedSession of [false, true]) {
const chat = renderPaperclipWakePrompt(payload, {
@@ -1170,7 +1170,7 @@ describe("renderPaperclipWakePrompt", () => {
expect(incompleteResumePrompt).toContain(
"[continuation summary truncated]",
);
expect(incompleteResumePrompt).toContain(
expect(incompleteResumePrompt).not.toContain(
"a successful process exit or final response is not sufficient",
);
});
@@ -1409,76 +1409,18 @@ describe("renderPaperclipWakePrompt", () => {
);
});
it("keeps the default local-agent prompt action-oriented", () => {
expect(DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE).toContain(
"Start actionable work in this heartbeat",
);
expect(DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE).toContain(
"do not stop at a plan",
);
expect(DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE).toContain(
"clear final disposition",
);
expect(DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE).toContain(
"evidence, not valid liveness paths by themselves",
);
expect(DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE).toContain(
"keep `in_progress` only when a live continuation path exists",
);
expect(DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE).toContain(
"Prefer the smallest verification that proves the change",
);
expect(DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE).toContain(
"After 2 consecutive failures of the same control-plane write",
);
expect(DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE).toContain(
"adapter/runtime status channel as the sanctioned fallback",
);
expect(DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE).toContain(
"Use child issues",
);
expect(DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE).toContain(
"instead of polling agents, sessions, or processes",
);
expect(DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE).toContain(
"Create child issues directly when you know what needs to be done",
);
expect(DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE).toContain(
"POST /api/issues/$PAPERCLIP_TASK_ID/interactions",
);
// URL paths in prompt text carry real ids or env vars, never brace
// placeholders: agents paste these lines verbatim, and a literal {issueId}
// reaches the server as /api/issues/%7BissueId%7D and 404s.
expect(DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE).not.toContain(
"/api/issues/{issueId}",
);
expect(DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE).not.toContain(
"/api/issues/{id}",
);
expect(DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE).toContain(
"kind suggest_tasks, ask_user_questions, or request_confirmation",
);
expect(DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE).toContain(
"Use continuationPolicy wake_assignee when you need to resume after a response (it wakes on acceptance and rejection alike; only expiry does not wake); use wake_assignee_on_accept when you want to resume only after acceptance",
);
expect(DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE).not.toContain(
"for request_confirmation this resumes only after acceptance",
);
expect(DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE).toContain(
"Never create probe or throwaway issue-thread interactions to discover the interactions API shape or your permissions",
);
expect(DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE).toContain(
"confirmation:{issueId}:plan:{revisionId}",
);
expect(DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE).toContain(
"Wait for acceptance before creating implementation subtasks",
);
expect(DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE).toContain(
"Respect budget, pause/cancel, approval gates, and company boundaries",
);
it("keeps task and chat defaults to identity and connection guidance", () => {
for (const template of [
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
DEFAULT_PAPERCLIP_CONVERSATION_PROMPT_TEMPLATE,
]) {
expect(template).toBe(
`You are agent {{agent.id}} ({{agent.name}}).\n\n${CONNECTION_INTENT_AGENT_GUIDANCE}`,
);
}
});
it("leaves the execution contract to the heartbeat template on fresh scoped wake prompts", () => {
it("keeps current task data without generic procedures on fresh scoped wake prompts", () => {
const prompt = renderPaperclipWakePrompt({
reason: "issue_assigned",
issue: {
@@ -1498,12 +1440,12 @@ describe("renderPaperclipWakePrompt", () => {
expect(prompt).toContain("## Paperclip Wake Payload");
expect(prompt).not.toContain("Execution contract:");
expect(DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE).toContain(
expect(DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE).not.toContain(
"Execution contract:",
);
});
it("adds the execution contract to resume delta prompts and opted-in fresh prompts", () => {
it("does not restore generic procedures on resume or with the legacy opt-in", () => {
const payload = {
reason: "issue_assigned",
issue: {
@@ -1521,36 +1463,19 @@ describe("renderPaperclipWakePrompt", () => {
fallbackFetchNeeded: false,
};
for (const prompt of [
renderPaperclipWakePrompt(payload, { resumedSession: true }),
renderPaperclipWakePrompt(payload, { includeExecutionContract: true }),
]) {
expect(prompt).toContain(
"Execution contract: take concrete action in this heartbeat",
);
expect(prompt).toContain("clear final disposition");
expect(prompt).toContain(
"Immediately before returning, verify that Paperclip records one of those dispositions",
);
expect(prompt).toContain(
"a successful process exit or final response is not sufficient",
);
expect(prompt).toContain(
"If no valid disposition is recorded, record it now and do not end the run",
);
expect(prompt).toContain(
"After 2 consecutive failures of the same control-plane write",
);
expect(prompt).toContain(
"adapter/runtime status channel as the sanctioned fallback",
);
expect(prompt).toContain(
"evidence, not valid liveness paths by themselves",
);
expect(prompt).toContain(
"Use child issues for long or parallel delegated work instead of polling",
);
expect(prompt).toContain("named unblock owner/action");
for (const resumedSession of [false, true]) {
for (const includeExecutionContract of [undefined, false, true]) {
const prompt = renderPaperclipWakePrompt(payload, {
resumedSession, includeExecutionContract,
});
expect(prompt).toContain(resumedSession ? "## Paperclip Resume Delta" : "## Paperclip Wake Payload");
expect(prompt).toContain("- reason: issue_assigned");
expect(prompt).toContain("- issue: PAP-1580 Update prompts");
expect(prompt).toContain("- issue status: in_progress");
expect(prompt).not.toContain("Execution contract:");
expect(prompt).not.toContain("clear final disposition");
expect(prompt).not.toContain("do not end the run");
}
}
});
@@ -1946,7 +1871,7 @@ describe("renderPaperclipWakePrompt", () => {
);
});
it("keeps exactly one execution contract in a composed fresh heartbeat prompt", () => {
it("keeps connection guidance without a generic manual in a composed fresh prompt", () => {
const wakePrompt = renderPaperclipWakePrompt({
reason: "issue_assigned",
issue: {
@@ -1966,7 +1891,8 @@ describe("renderPaperclipWakePrompt", () => {
const composed = [wakePrompt, DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE].join(
"\n\n",
);
expect(composed.match(/Execution contract/g)).toHaveLength(1);
expect(composed).toContain(CONNECTION_INTENT_AGENT_GUIDANCE);
expect(composed).not.toContain("Execution contract:");
});
it("trims comment-batch boilerplate on fresh wakes with zero pending comments", () => {
+11 -46
View File
@@ -207,41 +207,15 @@ export function resolvePaperclipInstanceRootForAdapter(
}
export const DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE = [
"You are agent {{agent.id}} ({{agent.name}}). Continue your Paperclip work.",
"",
"Execution contract:",
"- Start actionable work in this heartbeat; do not stop at a plan unless the issue asks for planning.",
"- Leave durable progress in comments, documents, or work products, then update the issue to a clear final disposition before ending the heartbeat.",
"- Comments, documents, screenshots, work products, and `Remaining` bullets are evidence, not valid liveness paths by themselves.",
"- Final disposition checklist: mark `done` when complete; use `in_review` only with a real reviewer, approval, interaction, or monitor path; use `blocked` only with first-class blockers or a named unblock owner/action; create delegated follow-up issues with blockers when another agent owns the next step; keep `in_progress` only when a live continuation path exists.",
"- Prefer the smallest verification that proves the change; do not default to full workspace typecheck/build/test on every heartbeat unless the task scope warrants it.",
"- After 2 consecutive failures of the same control-plane write, stop retrying that write for the rest of the heartbeat. Continue useful work, report the failure in the final response, and rely on the adapter/runtime status channel as the sanctioned fallback.",
"- Use child issues for parallel or long delegated work instead of polling agents, sessions, or processes.",
"- If woken by a human comment on a dependency-blocked issue, respond or triage the comment without treating the blocked deliverable work as unblocked.",
"- Create child issues directly when you know what needs to be done; use issue-thread interactions when the board/user must choose suggested tasks, answer structured questions, or confirm a proposal.",
"- Use `PAPERCLIP_SCRATCH_DIR` / `PAPERCLIP_RUN_SCRATCH_DIR` for temporary scratch files instead of ad hoc `/tmp` paths; Paperclip removes that run-owned directory after the run ends.",
"- To ask for that input, create an interaction on the current issue with POST /api/issues/$PAPERCLIP_TASK_ID/interactions using kind suggest_tasks, ask_user_questions, or request_confirmation. Use continuationPolicy wake_assignee when you need to resume after a response (it wakes on acceptance and rejection alike; only expiry does not wake); use wake_assignee_on_accept when you want to resume only after acceptance.",
"- Never create probe or throwaway issue-thread interactions to discover the interactions API shape or your permissions; schema discovery goes through the OpenAPI spec and explicit validation errors, not placeholder cards. Every ask_user_questions, suggest_tasks, or request_confirmation you post must carry a real, answerable prompt; withdraw one you no longer need instead of leaving it pending.",
"- When you intentionally restart follow-up work on a completed assigned issue, include structured `resume: true` with the POST /api/issues/$PAPERCLIP_TASK_ID/comments or PATCH /api/issues/$PAPERCLIP_TASK_ID comment payload (substitute that issue's real id when it is not the current task). Generic agent comments on closed issues are inert by default.",
"- For plan approval, update the plan document first, then create request_confirmation targeting the latest plan revision with idempotencyKey confirmation:{issueId}:plan:{revisionId}. Wait for acceptance before creating implementation subtasks, and create a fresh confirmation after superseding board/user comments if approval is still needed.",
"- If blocked, mark the issue blocked and name the unblock owner and action.",
"- Respect budget, pause/cancel, approval gates, and company boundaries.",
"- When the server-authenticated wake payload includes an External chat response contract, that narrower contract replaces the generic Paperclip comment, status, checkout, and final-disposition steps above for that turn. Follow the external-chat contract exactly; it does not relax any permission, approval, execution-policy, containment, budget, pause/cancel, or company boundary.",
"You are agent {{agent.id}} ({{agent.name}}).",
"",
CONNECTION_INTENT_AGENT_GUIDANCE,
].join("\n");
// Chat behavior is supplied centrally by the server's task-context markdown.
// Keep the ordinary task's completion/delegation contract out of this template.
export const DEFAULT_PAPERCLIP_CONVERSATION_PROMPT_TEMPLATE = [
"You are agent {{agent.id}} ({{agent.name}}). Continue your Paperclip conversation using the supplied chat mode directive.",
"Use available tools and assigned skills as needed; respect budget, pause/cancel, approval gates, and company boundaries.",
"Prefer the smallest verification that proves the action. Use PAPERCLIP_SCRATCH_DIR / PAPERCLIP_RUN_SCRATCH_DIR for temporary scratch files.",
"After 2 consecutive failures of the same control-plane write, stop retrying that write for the rest of the turn. Report the failure honestly; never claim an unconfirmed mutation succeeded.",
"Never create probe or throwaway issue-thread interactions. Every interaction must carry a real, answerable prompt; withdraw one you no longer need.",
"",
CONNECTION_INTENT_AGENT_GUIDANCE,
].join("\n");
// Task/chat modes arrive in server-owned context; operational procedures live
// in the harness-delivered Paperclip skill rather than either default template.
export const DEFAULT_PAPERCLIP_CONVERSATION_PROMPT_TEMPLATE =
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE;
export const WATCHDOG_DEFAULT_MANDATE = [
"You are running as a task watchdog, not as the original deliverable worker.",
@@ -2228,6 +2202,7 @@ export function selectPaperclipPromptSections(
options: {
resumedSession?: boolean;
includeCommunicationGuidance?: boolean;
/** @deprecated Generic execution instructions are no longer injected. */
includeExecutionContract?: boolean;
nativeWakeReaderAvailable?: boolean;
} = {},
@@ -2263,6 +2238,7 @@ function renderPaperclipWakePromptBody(
value: unknown,
options: {
resumedSession?: boolean;
/** @deprecated Generic execution instructions are no longer injected. */
includeExecutionContract?: boolean;
// Conversation policy arrives in the server-owned task markdown. Generic
// task disposition and child-delegation instructions conflict with it.
@@ -2287,12 +2263,6 @@ function renderPaperclipWakePromptBody(
externalChatTurn ||
externalChatReaderTurn ||
externalChatQuestionResponseTurn;
// The heartbeat prompt template already carries the execution contract on
// fresh sessions; only resume deltas (which replace the template) and
// template-less adapters need the wake-payload copy. An explicit false means
// another delivery carrier owns the contract, including on resume.
const includeExecutionContract = options.conversationMode !== true &&
(options.includeExecutionContract ?? resumedSession);
const hasWakeCommentBatch =
normalized.comments.length > 0 ||
normalized.includedCount > 0 ||
@@ -2383,7 +2353,7 @@ function renderPaperclipWakePromptBody(
}
};
const executionContractLines = externalChatContract
const wakeContractLines = externalChatContract
? [
externalChatQuestionResponseTurn
? "## External chat answered-question contract"
@@ -2430,12 +2400,7 @@ function renderPaperclipWakePromptBody(
`Fallback preference order: (1) send back to ${originalAssigneeLabel} with a retry instruction; (2) fix the runtime/adapter/workspace problem, then send it back; (3) reassign to another agent with the right specialty; (4) convert to an explicit manual-review state for the board.`,
"",
]
: includeExecutionContract
? [
"Execution contract: take concrete action in this heartbeat when the issue is actionable; do not stop at a plan unless planning was requested. Leave durable progress and then give the issue a clear final disposition before ending the heartbeat: `done`, `in_review` with a real reviewer/approval/interaction path, `blocked` with first-class blockers or a named unblock owner/action, delegated follow-up issues with blockers, or `in_progress` only when a live continuation path exists. Immediately before returning, verify that Paperclip records one of those dispositions; a successful process exit or final response is not sufficient. If no valid disposition is recorded, record it now and do not end the run. After 2 consecutive failures of the same control-plane write, stop retrying it for the rest of the heartbeat, continue useful work, report the failure in the final response, and rely on the adapter/runtime status channel as the sanctioned fallback. Use child issues for long or parallel delegated work instead of polling. Comments, documents, screenshots, work products, and `Remaining` bullets are evidence, not valid liveness paths by themselves.",
"",
]
: [];
: [];
const wakeSummaryLines = [
`- reason: ${normalized.reason ?? "unknown"}`,
`- issue: ${normalized.issue?.identifier ?? normalized.issue?.id ?? "unknown"}${normalized.issue?.title ? ` ${normalized.issue.title}` : ""}`,
@@ -2493,7 +2458,7 @@ function renderPaperclipWakePromptBody(
]),
"",
...externalInteractionContinuationLines,
...executionContractLines,
...wakeContractLines,
...wakeSummaryLines,
]
: [
@@ -2523,7 +2488,7 @@ function renderPaperclipWakePromptBody(
: []),
"",
...externalInteractionContinuationLines,
...executionContractLines,
...wakeContractLines,
...wakeSummaryLines,
];
@@ -164,7 +164,7 @@ describe("cursor_cloud execute", () => {
expect(result.exitCode).toBe(0);
const prompt = String(sdkAgent.send.mock.calls[0]?.[0]);
expect(prompt).toContain(directive);
expect(prompt).toContain(custom ? "Do the work for" : "Continue your Paperclip conversation");
expect(prompt).toContain(custom ? "Do the work for" : "You are agent agent-1");
expect(prompt).not.toContain("Execution contract:");
expect(prompt).not.toContain("Create child issues");
});
@@ -178,6 +178,9 @@ describe("cursor_cloud execute", () => {
expect(result.exitCode).toBe(0);
const prompt = String(sdkAgent.send.mock.calls[0]?.[0]);
expect(prompt).toContain("## Owned assignment");
expect(prompt).toContain("You are agent agent-1 (Cursor Cloud Agent).");
expect(prompt).toContain("Connection tools:");
expect(prompt).not.toContain("Execution contract:");
expect(prompt.indexOf("Append the same ledger entry.")).toBeLessThan(prompt.indexOf("Change the final scope to the launch checklist."));
expect(prompt.split("Append the same ledger entry.")).toHaveLength(3);
});
@@ -82,7 +82,7 @@ test("renders standard assignment wake with task authority and no backlog discov
expect(prompt).toContain("Paperclip task context:");
expect(prompt).toContain("Add focused unit tests for assignment wake and custom prompt rendering.");
expect(prompt).toContain("The harness already checked out this issue for the current run.");
expect(prompt).toContain("clear final disposition");
expect(prompt).not.toContain("clear final disposition");
expect(prompt).not.toContain("check for unassigned issues");
expect(prompt).not.toContain("status=backlog");
});
@@ -130,8 +130,8 @@ test("renders scoped planning wake authority before the Hermes default workflow"
expect(prompt).toContain("- checkout: already claimed by the harness for this run");
expect(prompt).toContain("The harness already checked out this issue for the current run.");
expect(prompt).toContain("Issue description:\n```text\nUse the wake payload as runtime authority.\n```");
expect(prompt).toContain("clear final disposition");
expect(prompt).toContain("keep `in_progress` only when a live continuation path exists");
expect(prompt).not.toContain("clear final disposition");
expect(prompt).not.toContain("keep `in_progress` only when a live continuation path exists");
expect(prompt).not.toContain("check for unassigned issues");
expect(prompt).not.toContain("status=backlog");
});
@@ -77,7 +77,7 @@ describe("OpenCode local skill injection", () => {
});
expect(result.exitCode).toBe(0);
expect(prompt).toContain(directive);
expect(prompt).toContain(custom ? "Custom agent instruction." : "Continue your Paperclip conversation");
expect(prompt).toContain(custom ? "Custom agent instruction." : "You are agent agent-1");
expect(prompt).not.toContain("Execution contract:");
expect(prompt).not.toContain("Create child issues");
});
@@ -102,6 +102,9 @@ describe("OpenCode local skill injection", () => {
expect(prompts).toHaveLength(2);
expect(prompts[0]).toContain("## Compact assignment");
expect(prompts[1]).toContain("## Owned assignment");
for (const prompt of prompts) expect(prompt).not.toContain("Execution contract:");
expect(prompts[1]).toContain("You are agent agent-1 (OpenCode).");
expect(prompts[1]).toContain("Connection tools:");
});
it("injects runtime skills into the configured child HOME", async () => {
@@ -696,7 +696,7 @@ describe("pi remote execution", () => {
expect(userPrompt).toBe("CUSTOM POLICY run-custom-policy");
});
it("keeps the resumed default execution contract in the system carrier only", async () => {
it("keeps resumed default identity and connection guidance in the system carrier", async () => {
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-pi-resumed-policy-"));
cleanupDirs.push(rootDir);
const sessionPath = path.join(rootDir, "session.jsonl");
@@ -726,11 +726,13 @@ describe("pi remote execution", () => {
const args = call?.[2] ?? [];
const systemPrompt = args[args.indexOf("--append-system-prompt") + 1] ?? "";
const userPrompt = args.at(-1) ?? "";
expect(systemPrompt).toContain("Execution contract:");
expect(systemPrompt).toContain("You are agent agent-1 (Pi Builder).");
expect(systemPrompt).toContain("Connection tools:");
expect(systemPrompt).not.toContain("Execution contract:");
expect(userPrompt).not.toContain("Execution contract:");
});
it("keeps the default contract in system input when custom prompt uses loaded instructions", async () => {
it("keeps default identity in system input when custom prompt uses loaded instructions", async () => {
const rootDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-pi-instructions-policy-"));
cleanupDirs.push(rootDir);
const sessionPath = path.join(rootDir, "session.jsonl");
@@ -769,7 +771,9 @@ describe("pi remote execution", () => {
const systemPrompt = args[args.indexOf("--append-system-prompt") + 1] ?? "";
const userPrompt = args.at(-1) ?? "";
expect(systemPrompt).toContain("Loaded instructions for this run.");
expect(systemPrompt).toContain("Execution contract:");
expect(systemPrompt).toContain("You are agent agent-1 (Pi Builder).");
expect(systemPrompt).toContain("Connection tools:");
expect(systemPrompt).not.toContain("Execution contract:");
expect(userPrompt).not.toContain("CUSTOM POLICY run-instructions-policy");
expect(userPrompt).not.toContain("Execution contract:");
});
@@ -1145,7 +1145,7 @@ describe.sequential("agent skill routes", () => {
);
});
it("materializes the bundled default instruction set for non-CEO agents with no prompt template", async () => {
it("materializes minimal default instructions for non-CEO agents with no prompt template", async () => {
const res = await requestApp(await createApp(), (baseUrl) => request(baseUrl)
.post("/api/companies/company-1/agents")
.send({
@@ -1165,38 +1165,10 @@ describe.sequential("agent skill routes", () => {
adapterType: "claude_local",
}),
expect.objectContaining({
"AGENTS.md": expect.stringMatching(/Start actionable work in the same heartbeat\.[\s\S]*Keep the work moving until it is done\./),
"AGENTS.md": "You are an agent in a Paperclip company.\n",
}),
{ entryFile: "AGENTS.md", replaceExisting: false },
);
expect(mockAgentInstructionsService.materializeManagedBundle).toHaveBeenCalledWith(
expect.any(Object),
expect.objectContaining({
"AGENTS.md": expect.stringContaining('kind: "request_confirmation"'),
}),
expect.any(Object),
);
expect(mockAgentInstructionsService.materializeManagedBundle).toHaveBeenCalledWith(
expect.any(Object),
expect.objectContaining({
"AGENTS.md": expect.stringContaining("confirmation:{issueId}:plan:{revisionId}"),
}),
expect.any(Object),
);
expect(mockAgentInstructionsService.materializeManagedBundle).toHaveBeenCalledWith(
expect.any(Object),
expect.objectContaining({
"AGENTS.md": expect.stringMatching(/PUT \/issues\/\{id\}\/documents\/plan[\s\S]*Re-`GET \/documents\/plan`, assert it returns `200`[\s\S]*latestRevisionId[\s\S]*target=\{ type: 'issue_document', key: 'plan', revisionId: latestRevisionId \}[\s\S]*Never present a plan only in a thread comment or through `ask_user_questions`/),
}),
expect.any(Object),
);
expect(mockAgentInstructionsService.materializeManagedBundle).toHaveBeenCalledWith(
expect.any(Object),
expect.objectContaining({
"AGENTS.md": expect.stringContaining("skills/paperclip/scripts/paperclip-upload-artifact.sh"),
}),
expect.any(Object),
);
});
});
@@ -1443,9 +1443,8 @@ process.exit(1);
expect(invocationPrompt).toContain("baseRevisionId set to that latestRevisionId");
expect(capture.prompt).not.toContain("Execution contract:");
expect(capture.prompt).not.toContain("Use child issues");
} else {
expect(capture.prompt).toContain("Execution contract:");
}
expect(capture.prompt).not.toContain("Execution contract:");
expect(capture.prompt).not.toContain("Follow the paperclip heartbeat.");
if (resumedSession) {
expect(capture.prompt).not.toContain("You are managed instructions.");
+1 -24
View File
@@ -1,24 +1 @@
You are an agent at Paperclip company.
## Execution Contract
- Start actionable work in the same heartbeat. Do not stop at a plan unless the issue explicitly asks for planning.
- Keep the work moving until it is done. If you need QA to review it, ask them. If you need your boss to review it, ask them.
- Leave durable progress in task comments, documents, or work products, then update the issue to a clear final disposition before you exit.
- When your work produces a user-inspectable deliverable file, follow the Paperclip skill's "Generated Artifacts and Work Products" workflow before final disposition. Use `skills/paperclip/scripts/paperclip-upload-artifact.sh` when working in this repo, create/update an artifact work product when the file is the deliverable, and link the uploaded attachment in the final comment. Do not rely on local filesystem paths as the only access path. If an important file intentionally remains workspace-only, create/update a work product with `metadata.resourceRef.kind: "workspace_file"` and a workspace-relative path, then name that work product and path in the final comment. Treat browse/search as a fallback for recovering workspace files, not the preferred deliverable path.
- When your work produces or updates an operator-facing engineering output, create/update the matching work product: `pull_request` for opened PRs, `preview_url` for published previews, `runtime_service` for managed preview/dev services, `commit` for notable pushed commits, and `branch` when the branch itself is the handoff. A comment is not a substitute for the work product access path.
- Comments, documents, screenshots, work products, and `Remaining` bullets are evidence, not valid liveness paths by themselves.
- Final disposition checklist: mark `done` when complete and verified; use `in_review` only with a real reviewer, approval, interaction, or monitor path; use `blocked` only with first-class blockers or a named unblock owner/action; create delegated follow-up issues with blockers when another agent owns the next step; keep `in_progress` only when a live continuation path exists.
- Use child issues for parallel or long delegated work instead of polling agents, sessions, or processes.
- Create child issues directly when you know what needs to be done. If the board/user needs to choose suggested tasks, answer structured questions, or confirm a proposal first, create an issue-thread interaction on the current issue with `POST /api/issues/{issueId}/interactions` using `kind: "suggest_tasks"`, `kind: "ask_user_questions"`, or `kind: "request_confirmation"`.
- Use `request_confirmation` instead of asking for yes/no decisions in markdown. Before presenting a plan for review, you MUST complete this publish contract:
1. `PUT /issues/{id}/documents/plan` with `{ format: 'markdown', body, changeSummary }`.
2. Re-`GET /documents/plan`, assert it returns `200`, and capture its `latestRevisionId`.
3. Only then create `request_confirmation` with `target={ type: 'issue_document', key: 'plan', revisionId: latestRevisionId }` and `idempotencyKey=confirmation:{issueId}:plan:{revisionId}`.
4. Wait for acceptance before creating implementation subtasks.
Never present a plan only in a thread comment or through `ask_user_questions`; comments are supporting context and questions are for gathering input, not plan review.
- `ask_user_questions` and confirmations default `supersedeOnUserComment` to `false`, so a later board/user comment keeps the pending card open while discussion continues. Set it to `true` when a new comment should replace the pending request. If you wake up from a superseding comment, revise the artifact, question set, or proposal and create a fresh interaction if input is still needed.
- For human input, save a pending question/confirmation interaction and set `in_review`; prose alone does not create a waiting path. Use `blockedByIssueIds` for issue dependencies. An agent may set an `unblockDescriptor` only for itself (`owner: { "agentId": "<your-agent-id>" }` plus `action`), not for the board/user or another agent.
- Respect budget, pause/cancel, approval gates, and company boundaries.
Do not let work sit here. You must always update your task with a comment.
You are an agent in a Paperclip company.
+2 -2
View File
@@ -2758,8 +2758,8 @@ export function agentRoutes(
// first-task/chief-of-staff/AGENTS.md, placeholders filled) over the agent's
// entry file instead of the generic default. Honored only for board-authored
// requests — the onboarding wizard runs as the board — so a client marker
// alone cannot swap another actor's instructions. The generic execution
// contract (default/AGENTS.md) is still appended on every run, unchanged.
// alone cannot swap another actor's instructions. Runtime coordination is
// supplied separately by the harness.
async function resolveOnboardingFirstAgentBundle(params: {
onboardingFirstAgent: unknown;
actorType: string;
@@ -139,8 +139,8 @@ export async function renderChiefOfStaffPersona(
}
// The instruction bundle for the onboarding first agent: the chief-of-staff
// persona as the entry AGENTS.md. The generic execution contract
// (default/AGENTS.md) is still appended on every run by the runner, unchanged.
// persona as the entry AGENTS.md. Runtime coordination is supplied separately
// by the harness.
export async function buildOnboardingFirstAgentInstructionsBundle(
placeholders: OnboardingFirstTaskPlaceholders,
): Promise<{ files: Record<string, string>; entryFile: string }> {