refactor: remove the wake payload from the launch environment
The server sent the wake payload two times: once as fenced JSON in the rendered prompt, and once as the PAPERCLIP_WAKE_PAYLOAD_JSON launch environment variable. Only the environment copy passed through the base64 launch envelope, and only that envelope has a per-string size limit (131,072 bytes). A normal driver task's payload can use over 100% of that limit on its own. The prompt is the single delivery path now. No production code read the environment variable; only the agent skill told the agent to check it. Every writer of the variable also rendered the prompt from the same normalized payload, so no content is lost. Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
9effe51b63
commit
fe4f55462c
|
|
@ -579,7 +579,9 @@ describe("shared ACPX engine runtime behavior", () => {
|
|||
expect(prompt).toContain("Paperclip runtime note:");
|
||||
expect(prompt).toContain("PAPERCLIP_AGENT_ID");
|
||||
expect(prompt).toContain("PAPERCLIP_API_KEY");
|
||||
expect(prompt).toContain("PAPERCLIP_WAKE_PAYLOAD_JSON");
|
||||
// The wake payload rides only the prompt body now, not a launch env var,
|
||||
// so it no longer appears in the PAPERCLIP_* runtime note list.
|
||||
expect(prompt).not.toContain("PAPERCLIP_WAKE_PAYLOAD_JSON");
|
||||
expect(prompt).toContain("Paperclip API access note:");
|
||||
expect(prompt).toContain('PAPERCLIP_API_BASE="${PAPERCLIP_API_URL%/}"; PAPERCLIP_API_BASE="${PAPERCLIP_API_BASE%/api}"');
|
||||
expect(prompt).toContain("$PAPERCLIP_API_BASE/api/agents/me");
|
||||
|
|
|
|||
|
|
@ -70,7 +70,6 @@ import {
|
|||
removeMaintainerOnlySkillSymlinks,
|
||||
rewriteWorkspaceCwdEnvVarsForExecution,
|
||||
shapePaperclipWorkspaceEnvForExecution,
|
||||
stringifyPaperclipWakePayload,
|
||||
type PaperclipSkillEntry,
|
||||
} from "@paperclipai/adapter-utils/server-utils";
|
||||
import { shellQuote } from "@paperclipai/adapter-utils/ssh";
|
||||
|
|
@ -1869,7 +1868,6 @@ async function buildRuntime(input: {
|
|||
const linkedIssueIds = Array.isArray(context.issueIds)
|
||||
? context.issueIds.filter((value): value is string => typeof value === "string" && value.trim().length > 0)
|
||||
: [];
|
||||
const wakePayloadJson = stringifyPaperclipWakePayload(context.paperclipWake);
|
||||
const issueWorkMode = readPaperclipIssueWorkModeFromContext(context);
|
||||
if (wakeTaskId) env.PAPERCLIP_TASK_ID = wakeTaskId;
|
||||
if (issueWorkMode) env.PAPERCLIP_ISSUE_WORK_MODE = issueWorkMode;
|
||||
|
|
@ -1878,7 +1876,6 @@ async function buildRuntime(input: {
|
|||
if (approvalId) env.PAPERCLIP_APPROVAL_ID = approvalId;
|
||||
if (approvalStatus) env.PAPERCLIP_APPROVAL_STATUS = approvalStatus;
|
||||
if (linkedIssueIds.length > 0) env.PAPERCLIP_LINKED_ISSUE_IDS = linkedIssueIds.join(",");
|
||||
if (wakePayloadJson) env.PAPERCLIP_WAKE_PAYLOAD_JSON = wakePayloadJson;
|
||||
applyPaperclipWorkspaceEnv(env, {
|
||||
workspaceCwd: shapedWorkspaceEnv.workspaceCwd,
|
||||
workspaceSource,
|
||||
|
|
|
|||
|
|
@ -455,6 +455,70 @@ describe("sandbox adapter execution targets", () => {
|
|||
}
|
||||
});
|
||||
|
||||
it("keeps the launch envelope under the kernel per-string limit even with a large env value", async () => {
|
||||
const rootDir = await mkdtemp(
|
||||
path.join(os.tmpdir(), "paperclip-process-session-envelope-"),
|
||||
);
|
||||
cleanupDirs.push(rootDir);
|
||||
const childPath = path.join(rootDir, "noop-acp-child.mjs");
|
||||
await writeFile(childPath, "process.stdin.on('data', () => {});\n", "utf8");
|
||||
|
||||
const delegate = createLocalSandboxRunner();
|
||||
const execScripts: string[] = [];
|
||||
const runner = {
|
||||
execute: vi.fn(async (input: Parameters<typeof delegate.execute>[0]) => {
|
||||
execScripts.push(input.args?.[1] ?? "");
|
||||
return delegate.execute(input);
|
||||
}),
|
||||
};
|
||||
const target: AdapterSandboxExecutionTarget = {
|
||||
kind: "remote",
|
||||
transport: "sandbox",
|
||||
providerKey: "local-test",
|
||||
remoteCwd: rootDir,
|
||||
timeoutMs: 30_000,
|
||||
runner,
|
||||
};
|
||||
|
||||
// A 90,000-byte single env value stands in for a wake payload sized
|
||||
// field. The old PAPERCLIP_WAKE_PAYLOAD_JSON writer put a value close to
|
||||
// this size into the launch env on a normal driver task.
|
||||
const largeEnvValue = "x".repeat(90_000);
|
||||
|
||||
const bridge = await startAdapterExecutionTargetProcessSessionBridge({
|
||||
runId: "run-process-session-envelope",
|
||||
target,
|
||||
runtimeRootDir: path.posix.join(rootDir, ".paperclip-runtime", "acpx"),
|
||||
adapterKey: "acpx",
|
||||
command: process.execPath,
|
||||
args: [childPath],
|
||||
cwd: rootDir,
|
||||
env: { PAPERCLIP_LARGE_FIELD: largeEnvValue },
|
||||
timeoutSec: 5,
|
||||
onLog: async () => {},
|
||||
});
|
||||
expect(bridge).not.toBeNull();
|
||||
|
||||
try {
|
||||
const launchExec = execScripts.find((script) =>
|
||||
script.includes("PAPERCLIP_PROCESS_SESSION_COMMAND_B64="),
|
||||
);
|
||||
expect(launchExec).toBeDefined();
|
||||
// shellQuote wraps the base64 payload in plain single quotes -- the
|
||||
// base64 alphabet has no single quote, so no escaping is present.
|
||||
const match = launchExec!.match(
|
||||
/PAPERCLIP_PROCESS_SESSION_COMMAND_B64='([^']*)'/,
|
||||
);
|
||||
expect(match).not.toBeNull();
|
||||
const commandPayloadBase64 = match![1];
|
||||
// A 32-page (4096-byte page) kernel MAX_ARG_STRLEN of 131,072 bytes
|
||||
// bounds a single argv/environ string, including this base64 payload.
|
||||
expect(commandPayloadBase64.length).toBeLessThan(131_072);
|
||||
} finally {
|
||||
await bridge?.stop();
|
||||
}
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ outputMode: "polled", streamOutputViaSession: false },
|
||||
{ outputMode: "streamed", streamOutputViaSession: true },
|
||||
|
|
|
|||
|
|
@ -2927,6 +2927,274 @@ describe("renderPaperclipWakePrompt", () => {
|
|||
expect(prompt).toContain("PAP-101 Implement helper (done)");
|
||||
expect(prompt).toContain("Added the helper route and tests.");
|
||||
});
|
||||
|
||||
// Locks the property the env-var removal (PAP-6176) depends on: the prompt
|
||||
// is the single delivery path, so every field the payload carries must
|
||||
// reach the prompt text. A handful of fields never render as prompt text
|
||||
// under any one scenario — `commentIds`/`interactionKind`/`interactionStatus`
|
||||
// only surface together on a planning wake with zero comments (mutually
|
||||
// exclusive with the comment-list scenario below), `unresolvedBlockerIssueIds`
|
||||
// is shadowed by `unresolvedBlockerSummaries` when both are set, and the
|
||||
// top-level `truncated` field is not read by the renderer at all. Those five
|
||||
// are still checked through the JSON round trip, proving the env-var copy
|
||||
// and the prompt copy come from the same normalized object.
|
||||
it("the prompt carries every top-level wake payload field", () => {
|
||||
const payload = {
|
||||
reason: "issue_commented",
|
||||
issue: {
|
||||
id: "wakecov-issue-id",
|
||||
identifier: "PAP-9001",
|
||||
title: "wakecov-issue-title",
|
||||
description: "wakecov-issue-description",
|
||||
descriptionTruncated: false,
|
||||
status: "wakecov-issue-status",
|
||||
workMode: "wakecov-issue-workmode",
|
||||
priority: "wakecov-issue-priority",
|
||||
},
|
||||
executionContinuation: {
|
||||
version: 1,
|
||||
companyId: "wakecov-company-id",
|
||||
issueId: "wakecov-issue-id",
|
||||
trigger: { reason: "issue_commented", interactionId: null, sourceRunId: null },
|
||||
originCommentIds: [],
|
||||
objective: "wakecov-objective",
|
||||
messages: [
|
||||
{
|
||||
id: "wakecov-message-id",
|
||||
authorType: "user",
|
||||
authorId: "wakecov-message-author",
|
||||
body: "wakecov-message-body",
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
deleted: false,
|
||||
sourceTrust: "trusted",
|
||||
},
|
||||
],
|
||||
interactionOutcomes: [
|
||||
{ id: "wakecov-outcome-id", kind: "wakecov-outcome-kind", status: "resolved", result: { note: "wakecov-interaction-result" } },
|
||||
],
|
||||
recoveryOutcomes: [{ recoveryActionId: "wakecov-recovery-action-id", decision: "retry" }],
|
||||
completedWork: "wakecov-completed-work",
|
||||
completedActions: [{ runId: "wakecov-run-id", receiptId: "wakecov-receipt-id", operationId: "wakecov-op-id", result: { ok: true } }],
|
||||
unresolvedInteractionIds: [],
|
||||
coverage: { kind: "full_task_history", throughCommentId: null, summaryThroughCommentId: null },
|
||||
},
|
||||
recovery: {
|
||||
cause: "process_lost",
|
||||
failureSummary: "wakecov-recovery-failure",
|
||||
originalAssignee: { id: "wakecov-orig-assignee-id", name: null },
|
||||
attemptCount: 2,
|
||||
maxAttempts: 5,
|
||||
nextAction: "wakecov-recovery-next-action",
|
||||
routingFallbackReason: "wakecov-routing-fallback-reason",
|
||||
},
|
||||
checkedOutByHarness: true,
|
||||
simplifiedEnglishInteractions: true,
|
||||
dependencyBlockedInteraction: true,
|
||||
treeHoldInteraction: true,
|
||||
activeTreeHold: { holdId: "wakecov-hold-id", rootIssueId: "wakecov-hold-root", mode: "wakecov-hold-mode" },
|
||||
unresolvedBlockerIssueIds: ["wakecov-blocker-issue-id"],
|
||||
unresolvedBlockerSummaries: [
|
||||
{ id: "wakecov-blocker-id", identifier: "wakecov-blocker-identifier", title: "wakecov-blocker-title", status: "wakecov-blocker-status" },
|
||||
],
|
||||
executionStage: {
|
||||
wakeRole: "reviewer",
|
||||
stageId: "wakecov-stage-id",
|
||||
stageType: "wakecov-stage-type",
|
||||
currentParticipant: { type: "agent", agentId: "wakecov-participant-agent" },
|
||||
returnAssignee: { type: "user", userId: "wakecov-return-user" },
|
||||
reviewRequest: { instructions: "wakecov-review-instructions" },
|
||||
lastDecisionOutcome: "wakecov-decision-outcome",
|
||||
allowedActions: ["wakecov-allowed-action"],
|
||||
},
|
||||
continuationSummary: { body: "wakecov-continuation-summary-body" },
|
||||
planReviewContext: null,
|
||||
documentReviewContext: null,
|
||||
annotationDeltas: [],
|
||||
livenessContinuation: {
|
||||
attempt: 3,
|
||||
maxAttempts: 6,
|
||||
sourceRunId: "wakecov-liveness-run-id",
|
||||
state: "wakecov-liveness-state",
|
||||
reason: "wakecov-liveness-reason",
|
||||
instruction: "wakecov-liveness-instruction",
|
||||
},
|
||||
taskWatchdog: null,
|
||||
interactionKind: "wakecov-interaction-kind-field",
|
||||
interactionStatus: "wakecov-interaction-status-field",
|
||||
checkboxSelection: {
|
||||
prompt: "wakecov-checkbox-prompt",
|
||||
selectedOptionIds: ["wakecov-option-id"],
|
||||
selectedOptions: [{ id: "wakecov-option-id", label: "wakecov-option-label", description: "wakecov-option-description" }],
|
||||
},
|
||||
questionResponse: {
|
||||
interactionId: "wakecov-question-interaction-id",
|
||||
summaryMarkdown: "wakecov-question-summary",
|
||||
},
|
||||
executionWorkspace: { branchName: "wakecov-branch-name" },
|
||||
agentMessage: { text: "wakecov-agent-message-text", source: "plugin", pluginKey: "wakecov-plugin-key" },
|
||||
childIssueSummaries: [
|
||||
{ id: "wakecov-child-id", identifier: "wakecov-child-identifier", title: "wakecov-child-title", status: "wakecov-child-status", summary: "wakecov-child-summary" },
|
||||
],
|
||||
childIssueSummaryTruncated: true,
|
||||
commentIds: ["wakecov-comment-id-a"],
|
||||
latestCommentId: "wakecov-latest-comment-id",
|
||||
comments: [
|
||||
{
|
||||
id: "wakecov-comment-id",
|
||||
body: "wakecov-comment-body",
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
author: { type: "user", id: "wakecov-comment-author-id" },
|
||||
},
|
||||
],
|
||||
commentWindow: { requestedCount: 3, includedCount: 1, missingCount: 2 },
|
||||
truncated: true,
|
||||
fallbackFetchNeeded: true,
|
||||
};
|
||||
|
||||
const serialized = stringifyPaperclipWakePayload(payload);
|
||||
expect(serialized).not.toBeNull();
|
||||
const parsed = JSON.parse(serialized ?? "{}");
|
||||
// Fields with no distinct prompt rendering under this scenario (see the
|
||||
// comment above the test): confirm the env-var copy still carried them.
|
||||
expect(parsed).toMatchObject({
|
||||
commentIds: ["wakecov-comment-id-a"],
|
||||
interactionKind: "wakecov-interaction-kind-field",
|
||||
interactionStatus: "wakecov-interaction-status-field",
|
||||
unresolvedBlockerIssueIds: ["wakecov-blocker-issue-id"],
|
||||
truncated: true,
|
||||
});
|
||||
|
||||
const prompt = renderPaperclipWakePrompt(payload);
|
||||
|
||||
// reason
|
||||
expect(prompt).toContain("- reason: issue_commented");
|
||||
// issue
|
||||
expect(prompt).toContain("- issue: PAP-9001 wakecov-issue-title");
|
||||
expect(prompt).toContain("wakecov-issue-description");
|
||||
expect(prompt).toContain("- issue status: wakecov-issue-status");
|
||||
expect(prompt).toContain("- issue work mode: wakecov-issue-workmode");
|
||||
expect(prompt).toContain("- issue priority: wakecov-issue-priority");
|
||||
// executionContinuation (objective, messages, completedWork, interactionOutcomes, completedActions, recoveryOutcomes)
|
||||
expect(prompt).toContain("wakecov-objective");
|
||||
expect(prompt).toContain("wakecov-message-body");
|
||||
expect(prompt).toContain("wakecov-completed-work");
|
||||
expect(prompt).toContain("wakecov-interaction-result");
|
||||
expect(prompt).toContain("wakecov-receipt-id");
|
||||
expect(prompt).toContain("wakecov-recovery-action-id");
|
||||
// recovery
|
||||
expect(prompt).toContain("- failure summary: wakecov-recovery-failure");
|
||||
expect(prompt).toContain("- original assignee: wakecov-orig-assignee-id");
|
||||
expect(prompt).toContain("- recovery attempt: 2/5");
|
||||
expect(prompt).toContain("- next action: wakecov-recovery-next-action");
|
||||
expect(prompt).toContain("- routing fallback: wakecov-routing-fallback-reason");
|
||||
// checkedOutByHarness
|
||||
expect(prompt).toContain("The harness already checked out this issue for the current run.");
|
||||
// simplifiedEnglishInteractions
|
||||
expect(prompt).toContain("ASD-STE100 Simplified Technical English");
|
||||
// dependencyBlockedInteraction + unresolvedBlockerSummaries
|
||||
expect(prompt).toContain("- dependency-blocked interaction: yes");
|
||||
expect(prompt).toContain("wakecov-blocker-identifier wakecov-blocker-title (wakecov-blocker-status)");
|
||||
// treeHoldInteraction + activeTreeHold
|
||||
expect(prompt).toContain("- tree-hold interaction: yes");
|
||||
expect(prompt).toContain("wakecov-hold-id rooted at wakecov-hold-root (wakecov-hold-mode)");
|
||||
// executionStage
|
||||
expect(prompt).toContain("- execution wake role: reviewer");
|
||||
expect(prompt).toContain("- execution stage: wakecov-stage-type");
|
||||
expect(prompt).toContain("agent wakecov-participant-agent");
|
||||
expect(prompt).toContain("user wakecov-return-user");
|
||||
expect(prompt).toContain("- last decision outcome: wakecov-decision-outcome");
|
||||
expect(prompt).toContain("wakecov-allowed-action");
|
||||
expect(prompt).toContain("wakecov-review-instructions");
|
||||
// continuationSummary
|
||||
expect(prompt).toContain("wakecov-continuation-summary-body");
|
||||
// livenessContinuation
|
||||
expect(prompt).toContain("- attempt: 3/6");
|
||||
expect(prompt).toContain("- source run: wakecov-liveness-run-id");
|
||||
expect(prompt).toContain("- liveness state: wakecov-liveness-state");
|
||||
expect(prompt).toContain("- reason: wakecov-liveness-reason");
|
||||
expect(prompt).toContain("- instruction: wakecov-liveness-instruction");
|
||||
// checkboxSelection
|
||||
expect(prompt).toContain("- checkbox prompt: wakecov-checkbox-prompt");
|
||||
expect(prompt).toContain("wakecov-option-id");
|
||||
expect(prompt).toContain("wakecov-option-label");
|
||||
expect(prompt).toContain("wakecov-option-description");
|
||||
// questionResponse
|
||||
expect(prompt).toContain("wakecov-question-interaction-id");
|
||||
expect(prompt).toContain("wakecov-question-summary");
|
||||
// executionWorkspace
|
||||
expect(prompt).toContain("wakecov-branch-name");
|
||||
// agentMessage
|
||||
expect(prompt).toContain("wakecov-plugin-key");
|
||||
expect(prompt).toContain("wakecov-agent-message-text");
|
||||
// childIssueSummaries + childIssueSummaryTruncated
|
||||
expect(prompt).toContain("wakecov-child-identifier wakecov-child-title (wakecov-child-status)");
|
||||
expect(prompt).toContain("wakecov-child-summary");
|
||||
expect(prompt).toContain("[child issue summaries truncated]");
|
||||
// comments, commentWindow (requestedCount/includedCount), latestCommentId, missingCount
|
||||
expect(prompt).toContain("wakecov-comment-id");
|
||||
expect(prompt).toContain("wakecov-comment-body");
|
||||
expect(prompt).toContain("- pending comments: 1/3");
|
||||
expect(prompt).toContain("- latest comment id: wakecov-latest-comment-id");
|
||||
expect(prompt).toContain("- omitted comments: 2");
|
||||
// fallbackFetchNeeded
|
||||
expect(prompt).toContain("- fallback fetch needed: yes");
|
||||
});
|
||||
|
||||
it("the prompt carries the resume delta on a resumed session", () => {
|
||||
const payload = {
|
||||
reason: "issue_commented",
|
||||
issue: { id: "wakecov-resume-issue-id", identifier: "PAP-9100", title: "Resume delta coverage" },
|
||||
executionContinuation: {
|
||||
version: 1,
|
||||
companyId: "wakecov-company-id",
|
||||
issueId: "wakecov-resume-issue-id",
|
||||
trigger: { reason: "issue_commented", interactionId: null, sourceRunId: null },
|
||||
originCommentIds: [],
|
||||
objective: "wakecov-resume-objective",
|
||||
messages: [
|
||||
{
|
||||
id: "wakecov-full-message-id",
|
||||
authorType: "user",
|
||||
authorId: "wakecov-message-author",
|
||||
body: "wakecov-full-history-message",
|
||||
createdAt: "2026-01-01T00:00:00.000Z",
|
||||
updatedAt: "2026-01-01T00:00:00.000Z",
|
||||
deleted: false,
|
||||
sourceTrust: "trusted",
|
||||
},
|
||||
],
|
||||
interactionOutcomes: [],
|
||||
completedWork: null,
|
||||
resumeDelta: {
|
||||
baseRunId: "wakecov-base-run-id",
|
||||
messages: [
|
||||
{
|
||||
id: "wakecov-delta-message-id",
|
||||
authorType: "user",
|
||||
authorId: "wakecov-message-author",
|
||||
body: "wakecov-delta-only-message",
|
||||
createdAt: "2026-01-02T00:00:00.000Z",
|
||||
updatedAt: "2026-01-02T00:00:00.000Z",
|
||||
deleted: false,
|
||||
sourceTrust: "trusted",
|
||||
},
|
||||
],
|
||||
},
|
||||
unresolvedInteractionIds: [],
|
||||
coverage: { kind: "full_task_history", throughCommentId: null, summaryThroughCommentId: null },
|
||||
},
|
||||
commentWindow: { requestedCount: 0, includedCount: 0, missingCount: 0 },
|
||||
comments: [],
|
||||
fallbackFetchNeeded: false,
|
||||
};
|
||||
|
||||
const prompt = renderPaperclipWakePrompt(payload, { resumedSession: true });
|
||||
expect(prompt).toContain("wakecov-delta-only-message");
|
||||
expect(prompt).toContain("task_history_delta");
|
||||
expect(prompt).toContain("wakecov-base-run-id");
|
||||
expect(prompt).not.toContain("wakecov-full-history-message");
|
||||
});
|
||||
});
|
||||
|
||||
describe("WATCHDOG_DEFAULT_MANDATE", () => {
|
||||
|
|
|
|||
|
|
@ -48,7 +48,6 @@ import {
|
|||
selectPaperclipTaskMarkdown,
|
||||
rewriteWorkspaceCwdEnvVarsForExecution,
|
||||
shapePaperclipWorkspaceEnvForExecution,
|
||||
stringifyPaperclipWakePayload,
|
||||
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
|
||||
} from "@paperclipai/adapter-utils/server-utils";
|
||||
import { buildSkillLibraryManifestMarkdown } from "@paperclipai/adapter-utils/skill-library-manifest";
|
||||
|
|
@ -240,7 +239,6 @@ async function buildClaudeRuntimeConfig(input: ClaudeExecutionInput): Promise<Cl
|
|||
const linkedIssueIds = Array.isArray(context.issueIds)
|
||||
? context.issueIds.filter((value): value is string => typeof value === "string" && value.trim().length > 0)
|
||||
: [];
|
||||
const wakePayloadJson = stringifyPaperclipWakePayload(context.paperclipWake);
|
||||
const issueWorkMode = readPaperclipIssueWorkModeFromContext(context);
|
||||
|
||||
if (wakeTaskId) {
|
||||
|
|
@ -264,9 +262,6 @@ async function buildClaudeRuntimeConfig(input: ClaudeExecutionInput): Promise<Cl
|
|||
if (linkedIssueIds.length > 0) {
|
||||
env.PAPERCLIP_LINKED_ISSUE_IDS = linkedIssueIds.join(",");
|
||||
}
|
||||
if (wakePayloadJson) {
|
||||
env.PAPERCLIP_WAKE_PAYLOAD_JSON = wakePayloadJson;
|
||||
}
|
||||
applyPaperclipWorkspaceEnv(env, {
|
||||
workspaceCwd: shapedWorkspaceEnv.workspaceCwd,
|
||||
workspaceSource,
|
||||
|
|
|
|||
|
|
@ -46,7 +46,6 @@ import {
|
|||
renderTemplate,
|
||||
renderPaperclipWakePrompt,
|
||||
isPaperclipRecoveryWakePayload,
|
||||
stringifyPaperclipWakePayload,
|
||||
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
|
||||
joinPromptSections,
|
||||
} from "@paperclipai/adapter-utils/server-utils";
|
||||
|
|
@ -897,7 +896,6 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
const linkedIssueIds = Array.isArray(context.issueIds)
|
||||
? context.issueIds.filter((value): value is string => typeof value === "string" && value.trim().length > 0)
|
||||
: [];
|
||||
const wakePayloadJson = stringifyPaperclipWakePayload(context.paperclipWake);
|
||||
const issueWorkMode = readPaperclipIssueWorkModeFromContext(context);
|
||||
if (wakeTaskId) {
|
||||
env.PAPERCLIP_TASK_ID = wakeTaskId;
|
||||
|
|
@ -920,9 +918,6 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
if (linkedIssueIds.length > 0) {
|
||||
env.PAPERCLIP_LINKED_ISSUE_IDS = linkedIssueIds.join(",");
|
||||
}
|
||||
if (wakePayloadJson) {
|
||||
env.PAPERCLIP_WAKE_PAYLOAD_JSON = wakePayloadJson;
|
||||
}
|
||||
refreshPaperclipWorkspaceEnvForExecution({
|
||||
env,
|
||||
envConfig,
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ import {
|
|||
renderPaperclipWakePrompt,
|
||||
isPaperclipRecoveryWakePayload,
|
||||
renderTemplate,
|
||||
stringifyPaperclipWakePayload,
|
||||
} from "@paperclipai/adapter-utils/server-utils";
|
||||
|
||||
type CursorCloudSession = {
|
||||
|
|
@ -122,7 +121,6 @@ function buildWakeEnv(ctx: AdapterExecutionContext, configEnv: Record<string, st
|
|||
const linkedIssueIds = Array.isArray(context.issueIds)
|
||||
? context.issueIds.filter((value): value is string => typeof value === "string" && value.trim().length > 0)
|
||||
: [];
|
||||
const wakePayloadJson = stringifyPaperclipWakePayload(context.paperclipWake);
|
||||
const issueWorkMode = readPaperclipIssueWorkModeFromContext(context);
|
||||
|
||||
if (wakeTaskId) env.PAPERCLIP_TASK_ID = wakeTaskId;
|
||||
|
|
@ -131,7 +129,6 @@ function buildWakeEnv(ctx: AdapterExecutionContext, configEnv: Record<string, st
|
|||
if (approvalId) env.PAPERCLIP_APPROVAL_ID = approvalId;
|
||||
if (approvalStatus) env.PAPERCLIP_APPROVAL_STATUS = approvalStatus;
|
||||
if (linkedIssueIds.length > 0) env.PAPERCLIP_LINKED_ISSUE_IDS = linkedIssueIds.join(",");
|
||||
if (wakePayloadJson) env.PAPERCLIP_WAKE_PAYLOAD_JSON = wakePayloadJson;
|
||||
if (issueWorkMode) env.PAPERCLIP_ISSUE_WORK_MODE = issueWorkMode;
|
||||
if (authToken) {
|
||||
env.PAPERCLIP_API_KEY = authToken;
|
||||
|
|
|
|||
|
|
@ -45,7 +45,6 @@ import {
|
|||
renderTemplate,
|
||||
renderPaperclipWakePrompt,
|
||||
isPaperclipRecoveryWakePayload,
|
||||
stringifyPaperclipWakePayload,
|
||||
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
|
||||
joinPromptSections,
|
||||
} from "@paperclipai/adapter-utils/server-utils";
|
||||
|
|
@ -270,7 +269,6 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
const linkedIssueIds = Array.isArray(context.issueIds)
|
||||
? context.issueIds.filter((value): value is string => typeof value === "string" && value.trim().length > 0)
|
||||
: [];
|
||||
const wakePayloadJson = stringifyPaperclipWakePayload(context.paperclipWake);
|
||||
const issueWorkMode = readPaperclipIssueWorkModeFromContext(context);
|
||||
if (wakeTaskId) {
|
||||
env.PAPERCLIP_TASK_ID = wakeTaskId;
|
||||
|
|
@ -293,9 +291,6 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
if (linkedIssueIds.length > 0) {
|
||||
env.PAPERCLIP_LINKED_ISSUE_IDS = linkedIssueIds.join(",");
|
||||
}
|
||||
if (wakePayloadJson) {
|
||||
env.PAPERCLIP_WAKE_PAYLOAD_JSON = wakePayloadJson;
|
||||
}
|
||||
refreshPaperclipWorkspaceEnvForExecution({
|
||||
env,
|
||||
envConfig,
|
||||
|
|
|
|||
|
|
@ -48,7 +48,6 @@ import {
|
|||
renderTemplate,
|
||||
renderPaperclipWakePrompt,
|
||||
isPaperclipRecoveryWakePayload,
|
||||
stringifyPaperclipWakePayload,
|
||||
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
|
||||
runChildProcess,
|
||||
} from "@paperclipai/adapter-utils/server-utils";
|
||||
|
|
@ -294,7 +293,6 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
const linkedIssueIds = Array.isArray(context.issueIds)
|
||||
? context.issueIds.filter((value): value is string => typeof value === "string" && value.trim().length > 0)
|
||||
: [];
|
||||
const wakePayloadJson = stringifyPaperclipWakePayload(context.paperclipWake);
|
||||
const issueWorkMode = readPaperclipIssueWorkModeFromContext(context);
|
||||
if (wakeTaskId) env.PAPERCLIP_TASK_ID = wakeTaskId;
|
||||
if (issueWorkMode) env.PAPERCLIP_ISSUE_WORK_MODE = issueWorkMode;
|
||||
|
|
@ -303,7 +301,6 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
if (approvalId) env.PAPERCLIP_APPROVAL_ID = approvalId;
|
||||
if (approvalStatus) env.PAPERCLIP_APPROVAL_STATUS = approvalStatus;
|
||||
if (linkedIssueIds.length > 0) env.PAPERCLIP_LINKED_ISSUE_IDS = linkedIssueIds.join(",");
|
||||
if (wakePayloadJson) env.PAPERCLIP_WAKE_PAYLOAD_JSON = wakePayloadJson;
|
||||
refreshPaperclipWorkspaceEnvForExecution({
|
||||
env,
|
||||
envConfig,
|
||||
|
|
|
|||
|
|
@ -36,7 +36,6 @@ import {
|
|||
renderPaperclipWakePrompt,
|
||||
isPaperclipRecoveryWakePayload,
|
||||
resolveLegacyPaperclipDesiredSkillNames,
|
||||
stringifyPaperclipWakePayload,
|
||||
refreshPaperclipWorkspaceEnvForExecution,
|
||||
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
|
||||
} from "@paperclipai/adapter-utils/server-utils";
|
||||
|
|
@ -281,7 +280,6 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
const linkedIssueIds = Array.isArray(context.issueIds)
|
||||
? context.issueIds.filter((value: unknown): value is string => typeof value === "string" && value.trim().length > 0)
|
||||
: [];
|
||||
const wakePayloadJson = stringifyPaperclipWakePayload(context.paperclipWake);
|
||||
const issueWorkMode = readPaperclipIssueWorkModeFromContext(context);
|
||||
if (wakeTaskId) env.PAPERCLIP_TASK_ID = wakeTaskId;
|
||||
if (issueWorkMode) env.PAPERCLIP_ISSUE_WORK_MODE = issueWorkMode;
|
||||
|
|
@ -290,7 +288,6 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
if (approvalId) env.PAPERCLIP_APPROVAL_ID = approvalId;
|
||||
if (approvalStatus) env.PAPERCLIP_APPROVAL_STATUS = approvalStatus;
|
||||
if (linkedIssueIds.length > 0) env.PAPERCLIP_LINKED_ISSUE_IDS = linkedIssueIds.join(",");
|
||||
if (wakePayloadJson) env.PAPERCLIP_WAKE_PAYLOAD_JSON = wakePayloadJson;
|
||||
refreshPaperclipWorkspaceEnvForExecution({
|
||||
env,
|
||||
envConfig,
|
||||
|
|
|
|||
|
|
@ -506,8 +506,6 @@ export async function execute(
|
|||
if (envWakeReason) env.PAPERCLIP_WAKE_REASON = envWakeReason;
|
||||
const envCommentId = cfgString(ctxContext.commentId) || cfgString(ctxContext.wakeCommentId) || cfgString(ctx.config?.commentId);
|
||||
if (envCommentId) env.PAPERCLIP_WAKE_COMMENT_ID = envCommentId;
|
||||
const wakePayloadJson = stringifyPaperclipWakePayload(ctxContext.paperclipWake);
|
||||
if (wakePayloadJson) env.PAPERCLIP_WAKE_PAYLOAD_JSON = wakePayloadJson;
|
||||
|
||||
// ── Resolve working directory ──────────────────────────────────────────
|
||||
const cwd =
|
||||
|
|
|
|||
|
|
@ -40,7 +40,6 @@ import {
|
|||
renderTemplate,
|
||||
renderPaperclipWakePrompt,
|
||||
isPaperclipRecoveryWakePayload,
|
||||
stringifyPaperclipWakePayload,
|
||||
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
|
||||
} from "@paperclipai/adapter-utils/server-utils";
|
||||
import {
|
||||
|
|
@ -267,7 +266,6 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
const linkedIssueIds = Array.isArray(context.issueIds)
|
||||
? context.issueIds.filter((value): value is string => typeof value === "string" && value.trim().length > 0)
|
||||
: [];
|
||||
const wakePayloadJson = stringifyPaperclipWakePayload(context.paperclipWake);
|
||||
const issueWorkMode = readPaperclipIssueWorkModeFromContext(context);
|
||||
if (wakeTaskId) env.PAPERCLIP_TASK_ID = wakeTaskId;
|
||||
if (issueWorkMode) env.PAPERCLIP_ISSUE_WORK_MODE = issueWorkMode;
|
||||
|
|
@ -276,7 +274,6 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
if (approvalId) env.PAPERCLIP_APPROVAL_ID = approvalId;
|
||||
if (approvalStatus) env.PAPERCLIP_APPROVAL_STATUS = approvalStatus;
|
||||
if (linkedIssueIds.length > 0) env.PAPERCLIP_LINKED_ISSUE_IDS = linkedIssueIds.join(",");
|
||||
if (wakePayloadJson) env.PAPERCLIP_WAKE_PAYLOAD_JSON = wakePayloadJson;
|
||||
refreshPaperclipWorkspaceEnvForExecution({
|
||||
env,
|
||||
envConfig,
|
||||
|
|
|
|||
|
|
@ -41,7 +41,6 @@ import {
|
|||
renderTemplate,
|
||||
renderPaperclipWakePrompt,
|
||||
isPaperclipRecoveryWakePayload,
|
||||
stringifyPaperclipWakePayload,
|
||||
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
|
||||
runChildProcess,
|
||||
isPaperclipSkillSourceMissing,
|
||||
|
|
@ -293,7 +292,6 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
const linkedIssueIds = Array.isArray(context.issueIds)
|
||||
? context.issueIds.filter((value): value is string => typeof value === "string" && value.trim().length > 0)
|
||||
: [];
|
||||
const wakePayloadJson = stringifyPaperclipWakePayload(context.paperclipWake);
|
||||
const issueWorkMode = readPaperclipIssueWorkModeFromContext(context);
|
||||
if (wakeTaskId) env.PAPERCLIP_TASK_ID = wakeTaskId;
|
||||
if (issueWorkMode) env.PAPERCLIP_ISSUE_WORK_MODE = issueWorkMode;
|
||||
|
|
@ -302,7 +300,6 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
if (approvalId) env.PAPERCLIP_APPROVAL_ID = approvalId;
|
||||
if (approvalStatus) env.PAPERCLIP_APPROVAL_STATUS = approvalStatus;
|
||||
if (linkedIssueIds.length > 0) env.PAPERCLIP_LINKED_ISSUE_IDS = linkedIssueIds.join(",");
|
||||
if (wakePayloadJson) env.PAPERCLIP_WAKE_PAYLOAD_JSON = wakePayloadJson;
|
||||
refreshPaperclipWorkspaceEnvForExecution({
|
||||
env,
|
||||
envConfig,
|
||||
|
|
|
|||
|
|
@ -46,7 +46,6 @@ import {
|
|||
renderTemplate,
|
||||
renderPaperclipWakePrompt,
|
||||
isPaperclipRecoveryWakePayload,
|
||||
stringifyPaperclipWakePayload,
|
||||
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
|
||||
runChildProcess,
|
||||
} from "@paperclipai/adapter-utils/server-utils";
|
||||
|
|
@ -298,7 +297,6 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
const linkedIssueIds = Array.isArray(context.issueIds)
|
||||
? context.issueIds.filter((value): value is string => typeof value === "string" && value.trim().length > 0)
|
||||
: [];
|
||||
const wakePayloadJson = stringifyPaperclipWakePayload(context.paperclipWake);
|
||||
const issueWorkMode = readPaperclipIssueWorkModeFromContext(context);
|
||||
|
||||
if (wakeTaskId) env.PAPERCLIP_TASK_ID = wakeTaskId;
|
||||
|
|
@ -308,7 +306,6 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
|||
if (approvalId) env.PAPERCLIP_APPROVAL_ID = approvalId;
|
||||
if (approvalStatus) env.PAPERCLIP_APPROVAL_STATUS = approvalStatus;
|
||||
if (linkedIssueIds.length > 0) env.PAPERCLIP_LINKED_ISSUE_IDS = linkedIssueIds.join(",");
|
||||
if (wakePayloadJson) env.PAPERCLIP_WAKE_PAYLOAD_JSON = wakePayloadJson;
|
||||
refreshPaperclipWorkspaceEnvForExecution({
|
||||
env,
|
||||
envConfig,
|
||||
|
|
|
|||
|
|
@ -315,8 +315,8 @@ The runner, package driver, and model/harness never receive:
|
|||
- the local agent JWT, `PAPERCLIP_API_KEY`, a board session, or a board API key;
|
||||
- managed MCP gateway credentials, runner-lease/bootstrap credentials, or
|
||||
credential-broker secret material;
|
||||
- `PAPERCLIP_WAKE_PAYLOAD_JSON`, rendered Paperclip wake text, Paperclip skill
|
||||
instructions, the Paperclip API manual, or run-scoped skill material;
|
||||
- rendered Paperclip wake text, Paperclip skill instructions, the Paperclip
|
||||
API manual, or run-scoped skill material;
|
||||
- raw `process.env`, agent/project/routine env maps, `runtimeConfig.env`, or the
|
||||
legacy adapter's generic execution context;
|
||||
- authority to choose a company, issue, agent, policy, approval, or status;
|
||||
|
|
|
|||
|
|
@ -624,19 +624,18 @@ describe("codex execute", () => {
|
|||
expect(result.errorMessage).toBeNull();
|
||||
|
||||
const capture = JSON.parse(await fs.readFile(capturePath, "utf8")) as CapturePayload;
|
||||
expect(capture.paperclipEnvKeys).toContain("PAPERCLIP_WAKE_PAYLOAD_JSON");
|
||||
expect(capture.paperclipWakePayloadJson).not.toBeNull();
|
||||
expect(JSON.parse(capture.paperclipWakePayloadJson ?? "{}")).toMatchObject({
|
||||
reason: "issue_commented",
|
||||
latestCommentId: "comment-2",
|
||||
commentIds: ["comment-1", "comment-2"],
|
||||
});
|
||||
// The prompt is the only delivery path for the wake payload now; the
|
||||
// launch env no longer carries a second copy.
|
||||
expect(capture.paperclipEnvKeys).not.toContain("PAPERCLIP_WAKE_PAYLOAD_JSON");
|
||||
expect(capture.paperclipWakePayloadJson).toBeNull();
|
||||
expect(capture.prompt).toContain("## Paperclip Wake Payload");
|
||||
expect(capture.prompt).toContain("Treat this wake payload as the highest-priority change for the current heartbeat.");
|
||||
expect(capture.prompt).toContain("Do not switch to another issue until you have handled this wake.");
|
||||
expect(capture.prompt).toContain(
|
||||
"acknowledge the latest comment and explain how it changes your next action.",
|
||||
);
|
||||
expect(capture.prompt).toContain("- reason: issue_commented");
|
||||
expect(capture.prompt).toContain("- latest comment id: comment-2");
|
||||
expect(capture.prompt).toContain("First comment");
|
||||
expect(capture.prompt).toContain("Second comment");
|
||||
} finally {
|
||||
|
|
@ -646,6 +645,95 @@ describe("codex execute", () => {
|
|||
}
|
||||
});
|
||||
|
||||
it("the launch env omits PAPERCLIP_WAKE_PAYLOAD_JSON", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-codex-execute-wake-env-"));
|
||||
const workspace = path.join(root, "workspace");
|
||||
const commandPath = path.join(root, "codex");
|
||||
const capturePath = path.join(root, "capture.json");
|
||||
await fs.mkdir(workspace, { recursive: true });
|
||||
await writeFakeCodexCommand(commandPath);
|
||||
|
||||
const previousHome = process.env.HOME;
|
||||
process.env.HOME = root;
|
||||
await seedSharedCodexAuth(root);
|
||||
|
||||
try {
|
||||
const result = await execute({
|
||||
runId: "run-wake-env",
|
||||
agent: {
|
||||
id: "agent-1",
|
||||
companyId: "company-1",
|
||||
name: "Codex Coder",
|
||||
adapterType: "codex_local",
|
||||
adapterConfig: { engine: "cli" },
|
||||
},
|
||||
runtime: {
|
||||
sessionId: null,
|
||||
sessionParams: null,
|
||||
sessionDisplayId: null,
|
||||
taskKey: null,
|
||||
},
|
||||
config: {
|
||||
engine: "cli",
|
||||
command: commandPath,
|
||||
cwd: workspace,
|
||||
env: {
|
||||
PAPERCLIP_TEST_CAPTURE_PATH: capturePath,
|
||||
},
|
||||
promptTemplate: "Follow the paperclip heartbeat.",
|
||||
},
|
||||
context: {
|
||||
issueId: "issue-1",
|
||||
taskId: "issue-1",
|
||||
wakeReason: "issue_commented",
|
||||
wakeCommentId: "comment-2",
|
||||
paperclipWake: {
|
||||
reason: "issue_commented",
|
||||
issue: {
|
||||
id: "issue-1",
|
||||
identifier: "PAP-874",
|
||||
title: "chat-speed issues",
|
||||
status: "in_progress",
|
||||
priority: "medium",
|
||||
},
|
||||
commentIds: ["comment-2"],
|
||||
latestCommentId: "comment-2",
|
||||
comments: [
|
||||
{
|
||||
id: "comment-2",
|
||||
issueId: "issue-1",
|
||||
body: "Second comment",
|
||||
bodyTruncated: false,
|
||||
createdAt: "2026-03-28T14:35:10.000Z",
|
||||
author: { type: "user", id: "user-1" },
|
||||
},
|
||||
],
|
||||
commentWindow: {
|
||||
requestedCount: 1,
|
||||
includedCount: 1,
|
||||
missingCount: 0,
|
||||
},
|
||||
truncated: false,
|
||||
fallbackFetchNeeded: false,
|
||||
},
|
||||
},
|
||||
authToken: "run-jwt-token",
|
||||
onLog: async () => {},
|
||||
});
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.errorMessage).toBeNull();
|
||||
|
||||
const capture = JSON.parse(await fs.readFile(capturePath, "utf8")) as CapturePayload;
|
||||
expect(capture.paperclipEnvKeys).not.toContain("PAPERCLIP_WAKE_PAYLOAD_JSON");
|
||||
expect(capture.paperclipWakePayloadJson).toBeNull();
|
||||
} finally {
|
||||
if (previousHome === undefined) delete process.env.HOME;
|
||||
else process.env.HOME = previousHome;
|
||||
await fs.rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("classifies remote-compaction high-demand failures as retryable transient upstream errors", async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-codex-execute-transient-"));
|
||||
const workspace = path.join(root, "workspace");
|
||||
|
|
@ -1181,26 +1269,19 @@ process.exit(1);
|
|||
expect(result.errorMessage).toBeNull();
|
||||
|
||||
const capture = JSON.parse(await fs.readFile(capturePath, "utf8")) as CapturePayload;
|
||||
expect(capture.paperclipEnvKeys).toContain("PAPERCLIP_WAKE_PAYLOAD_JSON");
|
||||
expect(capture.paperclipWakePayloadJson).not.toBeNull();
|
||||
expect(JSON.parse(capture.paperclipWakePayloadJson ?? "{}")).toMatchObject({
|
||||
reason: "issue_assigned",
|
||||
issue: {
|
||||
identifier: "PAP-1201",
|
||||
title: "Fix gallery opening for inline images",
|
||||
status: "in_progress",
|
||||
priority: "medium",
|
||||
},
|
||||
checkedOutByHarness: true,
|
||||
commentIds: [],
|
||||
});
|
||||
// The prompt is the only delivery path for the wake payload now; the
|
||||
// launch env no longer carries a second copy.
|
||||
expect(capture.paperclipEnvKeys).not.toContain("PAPERCLIP_WAKE_PAYLOAD_JSON");
|
||||
expect(capture.paperclipWakePayloadJson).toBeNull();
|
||||
expect(capture.prompt).toContain("## Paperclip Wake Payload");
|
||||
expect(capture.prompt).toContain("Do not switch to another issue until you have handled this wake.");
|
||||
expect(capture.prompt).toContain("- reason: issue_assigned");
|
||||
expect(capture.prompt).toContain("- issue: PAP-1201 Fix gallery opening for inline images");
|
||||
expect(capture.prompt).toContain("- issue status: in_progress");
|
||||
expect(capture.prompt).toContain("- issue priority: medium");
|
||||
expect(capture.prompt).not.toContain("- pending comments:");
|
||||
expect(capture.prompt).not.toContain("acknowledge the latest comment");
|
||||
expect(capture.prompt).not.toContain("Execution contract:");
|
||||
expect(capture.prompt).toContain("- issue status: in_progress");
|
||||
expect(capture.prompt).toContain("- checkout: already claimed by the harness for this run");
|
||||
expect(capture.prompt).toContain("The harness already checked out this issue for the current run.");
|
||||
} finally {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ In Paperclip, **task** and **issue** refer to the same work item. The UI may use
|
|||
|
||||
Env vars auto-injected: `PAPERCLIP_AGENT_ID`, `PAPERCLIP_COMPANY_ID`, `PAPERCLIP_API_URL`, `PAPERCLIP_RUN_ID`. Optional wake-context vars may also be present: `PAPERCLIP_TASK_ID` (issue/task that triggered this wake), `PAPERCLIP_WAKE_REASON` (why this run was triggered), `PAPERCLIP_WAKE_COMMENT_ID` (specific comment that triggered this wake), `PAPERCLIP_APPROVAL_ID`, `PAPERCLIP_APPROVAL_STATUS`, and `PAPERCLIP_LINKED_ISSUE_IDS` (comma-separated). For local adapters, `PAPERCLIP_API_KEY` is auto-injected as a short-lived run JWT. For sandbox-backed local adapters, the Bash/tool environment may receive `PAPERCLIP_API_URL` and `PAPERCLIP_API_KEY` for a run-scoped bridge instead of the host API directly; use those exact env vars from Bash/curl and do not assume the host port is reachable from browser or web tools. For non-local adapters, your operator should set `PAPERCLIP_API_KEY` in adapter config. All requests use `Authorization: Bearer $PAPERCLIP_API_KEY`. All endpoints under `/api`, all JSON. Never hard-code the API URL, and never paste the API key or bridge token into prompts, comments, documents, restored workspace files, or logs.
|
||||
|
||||
Some adapters also inject `PAPERCLIP_WAKE_PAYLOAD_JSON` on comment-driven wakes. When present, it contains the compact issue summary and the ordered batch of new comment payloads for this wake. Use it first. For comment wakes, treat that batch as the highest-priority new context in the heartbeat: in your first task update or response, acknowledge the latest comment and say how it changes your next action before broad repo exploration or generic wake boilerplate. Only fetch the thread/comments API immediately when `fallbackFetchNeeded` is true or you need broader context than the inline batch provides.
|
||||
The wake payload is part of this prompt, not a separate environment variable. Look for the `## Paperclip Wake Payload` or `## Paperclip Resume Delta` heading above. It holds the compact issue summary and the ordered batch of new comments for this wake. Use it first. For comment wakes, treat that batch as the highest-priority context in the heartbeat. In your first task update, acknowledge the latest comment. State how it changes your next action. Do this before broad repo exploration or generic wake text. Fetch the thread or comments API only when `fallbackFetchNeeded` is true. Also fetch it when you need more context than the inline batch gives.
|
||||
|
||||
Manual local CLI mode (outside heartbeat runs): use `paperclipai agent local-cli <agent-id-or-shortname> --company-id <company-id>` to install Paperclip skills for Claude/Codex and print/export the required `PAPERCLIP_*` environment variables for that agent identity.
|
||||
|
||||
|
|
@ -110,7 +110,7 @@ If already checked out by you, returns normally. If owned by another agent: `409
|
|||
|
||||
**Step 6 — Understand context.** Prefer `GET /api/issues/{issueId}/heartbeat-context` first. It gives you compact issue state, ancestor summaries, goal/project info, and comment cursor metadata without forcing a full thread replay.
|
||||
|
||||
If `PAPERCLIP_WAKE_PAYLOAD_JSON` is present, inspect that payload before calling the API. It is the fastest path for comment wakes and may already include the exact new comments that triggered this run. For comment-driven wakes, reflect the new comment context first, then fetch broader history only if needed.
|
||||
Read the wake payload block of this prompt before you call the API. Look for the `## Paperclip Wake Payload` or `## Paperclip Resume Delta` heading. It is the fastest path for comment wakes. It may already hold the exact new comments that triggered this run. For comment-driven wakes, reflect the new comment context first. Then fetch broader history only if you need it.
|
||||
|
||||
Use comments incrementally:
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue