diff --git a/packages/adapter-utils/src/server-utils.ts b/packages/adapter-utils/src/server-utils.ts index 8b2f0209758..d5614c6f047 100644 --- a/packages/adapter-utils/src/server-utils.ts +++ b/packages/adapter-utils/src/server-utils.ts @@ -207,15 +207,41 @@ export function resolvePaperclipInstanceRootForAdapter( } export const DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE = [ - "You are agent {{agent.id}} ({{agent.name}}).", + "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.", "", 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; +// 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"); export const WATCHDOG_DEFAULT_MANDATE = [ "You are running as a task watchdog, not as the original deliverable worker.", @@ -2202,7 +2228,6 @@ export function selectPaperclipPromptSections( options: { resumedSession?: boolean; includeCommunicationGuidance?: boolean; - /** @deprecated Generic execution instructions are no longer injected. */ includeExecutionContract?: boolean; nativeWakeReaderAvailable?: boolean; } = {}, @@ -2238,7 +2263,6 @@ 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. @@ -2263,6 +2287,12 @@ 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 || @@ -2353,7 +2383,7 @@ function renderPaperclipWakePromptBody( } }; - const wakeContractLines = externalChatContract + const executionContractLines = externalChatContract ? [ externalChatQuestionResponseTurn ? "## External chat answered-question contract" @@ -2400,7 +2430,12 @@ 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}` : ""}`, @@ -2458,7 +2493,7 @@ function renderPaperclipWakePromptBody( ]), "", ...externalInteractionContinuationLines, - ...wakeContractLines, + ...executionContractLines, ...wakeSummaryLines, ] : [ @@ -2488,7 +2523,7 @@ function renderPaperclipWakePromptBody( : []), "", ...externalInteractionContinuationLines, - ...wakeContractLines, + ...executionContractLines, ...wakeSummaryLines, ]; diff --git a/server/src/onboarding-assets/default/AGENTS.md b/server/src/onboarding-assets/default/AGENTS.md index 8c6df421c78..14724839144 100644 --- a/server/src/onboarding-assets/default/AGENTS.md +++ b/server/src/onboarding-assets/default/AGENTS.md @@ -1 +1,24 @@ -You are an agent in a Paperclip company. +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": "" }` 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. diff --git a/tests/runner-e2e/catalog.ts b/tests/runner-e2e/catalog.ts index 54ee749e76f..ed7a0932de6 100644 --- a/tests/runner-e2e/catalog.ts +++ b/tests/runner-e2e/catalog.ts @@ -1225,7 +1225,8 @@ export const runnerSuites: readonly RunnerSuiteFixture[] = [ tasks: [...contextIntegrityTasks, chatTasks.find(task => task.id === "continuity-restart")!], expectedMatrixSize: 24, definitionMetadata: { - version: 1, instructions: "production-default-hire", scheduling: "explicit-only", + version: 1, instructions: "historical-default-hire-comparison", scheduling: "explicit-only", + comparison: { variant: "previous-instructions", candidateSha: "a63437069de58d22ee5adbcb6a6202c007dcf037", restoredFrom: "e00d10d5d5594f6e2d1e8cf4e0ec81c074b0bd88", nativeCodexFix: "held-constant-14920" }, sourceDigest: stockHarnessSourceDigest(), grading: "public-default-bundle-and-delivered-prompts-plus-independent-lifecycle-oracles", vendorBaseEvidence: "required deterministic Codex driver/runnerd/Rust gate; task success is not vendor-base proof", diff --git a/tests/runner-e2e/stock-harness-comparison.json b/tests/runner-e2e/stock-harness-comparison.json new file mode 100644 index 00000000000..03e66157010 --- /dev/null +++ b/tests/runner-e2e/stock-harness-comparison.json @@ -0,0 +1,29 @@ +{ + "schema": "paperclip.stock-harness-comparison.v1", + "variant": "previous-instructions", + "candidateSha": "a63437069de58d22ee5adbcb6a6202c007dcf037", + "restoredInstructionsFrom": "e00d10d5d5594f6e2d1e8cf4e0ec81c074b0bd88", + "nativeCodex14920": "held-constant; no before/after performance claim", + "productionDifferences": [ + "server/src/onboarding-assets/default/AGENTS.md", + "packages/adapter-utils/src/server-utils.ts" + ], + "structuralOracle": "Historical manual exact content and observed generic procedures; independent behavioral graders unchanged", + "expectedMatrixCells": 24, + "expectedProviderTurns": 48, + "maxInitialCampaignsPerVariant": 1, + "behavioralSources": { + "context-integrity-cases.ts": "201d05f8257c1121a94dd9d8a22f08b9db80555e388013ffeabb65e301499fd2", + "context-integrity-scoring.ts": "50d370044e45386ba87fb23ca63c228b96bb8373d67bb7e82e8b2a89eb23f252", + "context-integrity-flow.ts": "63bcb7bdba4c80b15ff5872f16383844456eab0c5a10627da5d3487a26821c6a", + "chat-cases.ts": "754b251bdd42605e147da9081b663329234260d2b13df0df3d44562e362b14ca", + "chat-flow.ts": "11fc506559da61315ce04a877f6b38a055e71e532fecfcf62c749a1d3bf985e7", + "live-fixtures.ts": "1ef80209cc53cc514a8e58af4c2225e38ec15b20acf17ca7564506ca6617ac3e", + "runner.spec.ts": "41090ca983607edb2a6c21975dccf6ee84dde723ad31f60b7f39c42b65446fa1", + "types.ts": "c45dc3b2366457449ec9e7d413f930605dec88ea93a14ce5c5a5f445a27d148c" + }, + "instructionSha256": { + "manual": "e4d2375d722602cd744403292d99f6e6c9b7e8014d9cdfa6f9811ace03428e5f", + "shared": "819026782b92f37064fca354ddbca685b3c05f1869ec560e40d8a757b0a8aee9" + } +} diff --git a/tests/runner-e2e/stock-harness.test.ts b/tests/runner-e2e/stock-harness.test.ts index 37776355e84..954b17eb856 100644 --- a/tests/runner-e2e/stock-harness.test.ts +++ b/tests/runner-e2e/stock-harness.test.ts @@ -9,7 +9,7 @@ function recording(generation: "legacy" | "native" = "legacy"): StockHarnessEvid bundle: { entryFile: "AGENTS.md", files: [{ path: "AGENTS.md", content: STOCK_HIRE_IDENTITY }] }, runIds: ["fresh", "resumed"], invocations: generation === "native" ? [] : [ - { runId: "fresh", prompt: "You are agent agent (QA).\nConnection tools:\nUse connections_search.\nCurrent assignment.", promptMetrics: { heartbeatPromptChars: 113 } }, + { runId: "fresh", prompt: "You are agent agent (QA).\nConnection tools:\nUse connections_search.\nCurrent assignment.\nAfter 2 consecutive failures of the same control-plane write", promptMetrics: { heartbeatPromptChars: 113 } }, { runId: "resumed", prompt: "Paperclip Resume Delta\nCurrent ordered comments.", promptMetrics: { heartbeatPromptChars: 0 } }, ], }; @@ -67,8 +67,7 @@ describe("stock harness Product E2E", () => { ["missing-invocation", (e: StockHarnessEvidence) => { e.invocations.pop(); }, "invocation-evidence-complete"], ["empty-prompt", (e: StockHarnessEvidence) => { e.invocations[1]!.prompt = ""; }, "invocation-evidence-complete"], ["malformed-prompt", (e: StockHarnessEvidence) => { e.invocations[1]!.prompt = { text: "hidden" }; }, "invocation-evidence-complete"], - ["old-startup", (e: StockHarnessEvidence) => { e.invocations[0]!.prompt += "\nExecution contract:"; }, "generic-procedures-absent"], - ["old-resume", (e: StockHarnessEvidence) => { e.invocations[1]!.prompt += "\na successful process exit or final response is not sufficient"; }, "generic-procedures-absent"], + ["missing-historical-procedures", (e: StockHarnessEvidence) => { e.invocations[0]!.prompt = "You are agent agent. Connection tools: connections_search"; }, "historical-generic-procedures-observed"], ["missing-connections", (e: StockHarnessEvidence) => { e.invocations[0]!.prompt = "You are agent agent."; }, "fresh-default-delivered"], ["missing-fresh-evidence", (e: StockHarnessEvidence) => { e.invocations[0]!.promptMetrics = {}; }, "fresh-default-delivered"], ["wrong-budget", (e: StockHarnessEvidence) => { e.budgets.agentMonthlyCents = 0; }, "budget-hard-stops"], diff --git a/tests/runner-e2e/stock-harness.ts b/tests/runner-e2e/stock-harness.ts index 5993e06cb17..2e9dcf5933f 100644 --- a/tests/runner-e2e/stock-harness.ts +++ b/tests/runner-e2e/stock-harness.ts @@ -2,9 +2,9 @@ import { createHash } from "node:crypto"; import { readFileSync } from "node:fs"; import type { RunnerProfileFixture } from "./types.js"; -// These are an independent product contract, not imports of the implementation -// constants. Otherwise a larger shipped manual could silently update the oracle. -export const STOCK_HIRE_IDENTITY = "You are an agent in a Paperclip company.\n"; +// This temporary comparison revision independently pins the historical manual. +// Candidate instruction assertions remain unchanged on the candidate branch. +export const STOCK_HIRE_IDENTITY = "You are an agent at Paperclip company.\n\n## Execution Contract\n\n- Start actionable work in the same heartbeat. Do not stop at a plan unless the issue explicitly asks for planning.\n- 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.\n- Leave durable progress in task comments, documents, or work products, then update the issue to a clear final disposition before you exit.\n- 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.\n- 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.\n- Comments, documents, screenshots, work products, and `Remaining` bullets are evidence, not valid liveness paths by themselves.\n- 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.\n- Use child issues for parallel or long delegated work instead of polling agents, sessions, or processes.\n- 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\"`.\n- Use `request_confirmation` instead of asking for yes/no decisions in markdown. Before presenting a plan for review, you MUST complete this publish contract:\n 1. `PUT /issues/{id}/documents/plan` with `{ format: 'markdown', body, changeSummary }`.\n 2. Re-`GET /documents/plan`, assert it returns `200`, and capture its `latestRevisionId`.\n 3. Only then create `request_confirmation` with `target={ type: 'issue_document', key: 'plan', revisionId: latestRevisionId }` and `idempotencyKey=confirmation:{issueId}:plan:{revisionId}`.\n 4. Wait for acceptance before creating implementation subtasks.\n 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.\n- `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.\n- 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\": \"\" }` plus `action`), not for the board/user or another agent.\n- Respect budget, pause/cancel, approval gates, and company boundaries.\n\nDo not let work sit here. You must always update your task with a comment.\n"; export const STOCK_TEMPLATE_IDENTITY = "You are agent"; const REMOVED_PROCEDURES = [ "Execution contract:", @@ -43,7 +43,7 @@ export function gradeStockHire(evidence: Pick 0 && evidence.runIds.every(runId => prompts.some(row => row.runId === runId)), "Every legacy run must have a public adapter.invoke event with its actual nonempty prompt."); - check("generic-procedures-absent", prompts.length > 0 && prompts.every(row => - REMOVED_PROCEDURES.every(procedure => !String(row.prompt).includes(procedure))), - "Neither startup nor continuation may reintroduce the removed generic manual."); + check("historical-generic-procedures-observed", prompts.length > 0 && prompts.some(row => + REMOVED_PROCEDURES.some(procedure => String(row.prompt).includes(procedure))), + "Baseline structural receipt observes the historical procedures; identical lifecycle graders own task performance."); const fresh = prompts.filter(row => Number(row.promptMetrics?.heartbeatPromptChars) > 0); check("fresh-default-delivered", fresh.length > 0 && fresh.every(row => String(row.prompt).includes(STOCK_TEMPLATE_IDENTITY) && String(row.prompt).includes("Connection tools:") &&