mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-02 02:07:25 +08:00
Stop duplicating wake context in adapter environments (#13891)
## Thinking Path
> - Paperclip manages agent work and preserves task context.
> - Built-in adapters already include wake context in the agent prompt.
> - They also copy the full wake JSON into a process environment
variable.
> - A large environment entry can prevent the agent from starting with
`spawn E2BIG`.
> - This change removes the duplicate environment entry and uses the
existing prompt delivery.
> - The agent keeps its context without extra file transport or new
history limits.
## Linked Issues or Issue Description
Refs #13144, #13860, #13872, #13793.
Large wake payloads can exceed the operating system limit for one
environment entry. The launch-envelope fix in #13793 handles the outer
transport but leaves that child environment entry intact.
Credit to @nickyleach for the prompt-only approach in #13144. This PR
applies that part on current master. It does not include that PR's
30-item history limits or recovery-history endpoint. Those behavior
changes can be reviewed separately from the process launch fix.
## What Changed
- Stop exporting `PAPERCLIP_WAKE_PAYLOAD_JSON` in the shared ACP engine
and all ten built-in adapter writers.
- Ignore configured values of the retired variable so saved adapter
settings cannot restore the oversized entry. Also drop inherited copies
in Hermes, which builds its environment directly.
- Keep scalar runtime variables, existing prompt rendering, continuation
history, resume deltas, gateway bodies, and Hermes JSON template
variables.
- Document the prompt delivery contract and the migration for custom
instructions that read the retired variable.
- Test large local and sandbox child-process launches, fresh and resumed
ACP turns, SDK delivery, and configured-variable filtering.
## Verification
- `pnpm -r typecheck` passed.
- Focused adapter utility, ACP, Codex child-process, and Cursor Cloud
suites: 340 tests passed.
- Hermes execution and prompt tests: 18 tests passed using its package
Vitest configuration.
- The child-process tests deliver over 128 KB of context through stdin
and check the complete text. The ACP test retains 50 complete messages
and 50 completed actions, then checks the resumed delta.
- `pnpm build` passed.
- `pnpm test:run` was attempted, then stopped after it reproduced ten
macOS runtime-skill-cache permission failures (also reproduced on
unchanged master) and one HTTPS backfill test failure. The HTTPS test
passed when rerun unchanged on this branch and master. The complete
local suite was not completed; Linux CI provides the full-suite gate.
- CI is green on c3b8191f97: 53 checks passed and two skipped. Attempt 3
passed the remaining shards without source changes. Earlier attempts hit
a lost runner, preview readiness, and chat browser failures. The preview
test passed unchanged locally. Local chat tests passed 3/4; the
remaining sidebar focus-style assertion also fails on unchanged master.
No tests were weakened or skipped to obtain the passing rerun.
- The staged diff passed `gitleaks stdin --redact` and `git diff
--check`.
## Risks
- Custom instructions or scripts that read `PAPERCLIP_WAKE_PAYLOAD_JSON`
must use the wake payload in the prompt instead. The variable is absent
even for small wakes.
- This removes one cause of `E2BIG`. Legacy CLI paths for Gemini, Grok,
Kimi, Pi, and Hermes still pass prompts as arguments and retain their
existing argument-size limits. Other large environment variables also
remain subject to OS limits.
- No new context truncation, API route, database migration,
authorization change, or production rollout is part of this PR. Existing
prompt windows and resume rendering remain unchanged.
## Model Used
OpenAI GPT-6 through Codex, with code execution and repository tools.
The exact model variant and context window size are not exposed in this
session.
## Checklist
- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge
---------
Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -989,6 +989,25 @@ In Vite middleware mode, Paperclip gives HMR a dedicated HTTP server bound to th
|
||||
|
||||
When a workspace service runs Paperclip for browser OAuth QA, configure its `expose.urlTemplate` with the canonical URL the browser can reach. Paperclip preserves explicit `PAPERCLIP_PUBLIC_URL` or `BETTER_AUTH_URL` settings; otherwise it uses a valid exposed HTTPS origin (or loopback HTTP) as the managed runtime fallback for Better Auth and `/api/tools/oauth/callback`. Internal service names such as `http://paperclip-dev:<port>` are rejected unless that hostname is genuinely the browser route. Use a unique origin per isolated worktree. See [Execution Workspaces And Runtime Services](../docs/guides/board-operator/execution-workspaces-and-runtime-services.md#browser-reachable-origins-for-oauth-qa) for configuration and verification.
|
||||
|
||||
## Wake Context Delivery
|
||||
|
||||
Built-in adapters deliver wake context through the run prompt, including structured
|
||||
execution-continuation data. They do not export `PAPERCLIP_WAKE_PAYLOAD_JSON`. A
|
||||
large JSON environment entry can prevent the agent process from starting with
|
||||
`E2BIG`, even when the same context fits in the prompt transport. Configured values
|
||||
for this retired variable are ignored. Scalar runtime variables such as
|
||||
`PAPERCLIP_TASK_ID` and `PAPERCLIP_WAKE_REASON` remain available.
|
||||
|
||||
Custom instructions that read the retired variable must use the wake payload in
|
||||
the prompt instead. This transport change adds no history limits or truncation;
|
||||
existing comment windows and resume-delta rendering still apply. Gateway request
|
||||
bodies and Hermes prompt-template JSON variables remain supported.
|
||||
|
||||
This removes the duplicate environment entry, not every possible `E2BIG` cause.
|
||||
Legacy CLI paths that put prompts in command-line arguments (Gemini, Grok, Kimi,
|
||||
Pi, and Hermes) still have argument-size limits. ACP turns, SDK requests, and
|
||||
CLI paths that use stdin avoid that separate limit for the wake prompt.
|
||||
|
||||
## Paperclip Runner Adapter Conversion
|
||||
|
||||
The experimental Paperclip Runner offers native Codex, OpenCode, and **ACPX
|
||||
|
||||
@@ -133,6 +133,7 @@ function createLocalSandboxRunner(
|
||||
function buildRuntime(
|
||||
onSetConfigOption?: (input: { key: string; value: string }) => void,
|
||||
onEnsureSession?: (input: Record<string, unknown>) => void,
|
||||
onStartTurn?: (input: Record<string, unknown>) => void,
|
||||
) {
|
||||
return {
|
||||
ensureSession: async (input: Record<string, unknown>) => {
|
||||
@@ -143,13 +144,16 @@ function buildRuntime(
|
||||
runtimeSessionName: "runtime-session",
|
||||
});
|
||||
},
|
||||
startTurn: () => ({
|
||||
events: (async function* () {
|
||||
yield { type: "done", stopReason: "end_turn" };
|
||||
})(),
|
||||
result: Promise.resolve({ status: "completed", stopReason: "end_turn" }),
|
||||
cancel: async () => {},
|
||||
}),
|
||||
startTurn: (input: Record<string, unknown>) => {
|
||||
onStartTurn?.(input);
|
||||
return {
|
||||
events: (async function* () {
|
||||
yield { type: "done", stopReason: "end_turn" };
|
||||
})(),
|
||||
result: Promise.resolve({ status: "completed", stopReason: "end_turn" }),
|
||||
cancel: async () => {},
|
||||
};
|
||||
},
|
||||
setConfigOption: async (input: { key: string; value: string }) => {
|
||||
onSetConfigOption?.(input);
|
||||
},
|
||||
@@ -173,6 +177,7 @@ async function runExecutor(
|
||||
const runtimeOptions: Record<string, unknown>[] = [];
|
||||
const configOptions: Array<{ key: string; value: string }> = [];
|
||||
const sessionInputs: Record<string, unknown>[] = [];
|
||||
const turnInputs: Record<string, unknown>[] = [];
|
||||
const meta: Record<string, unknown>[] = [];
|
||||
const logs: Array<{ stream: string; text: string }> = [];
|
||||
const events: Array<{ eventType: string; payload?: Record<string, unknown> }> = [];
|
||||
@@ -185,6 +190,7 @@ async function runExecutor(
|
||||
return buildRuntime(
|
||||
({ key, value }) => configOptions.push({ key, value }),
|
||||
(input) => sessionInputs.push(input),
|
||||
(input) => turnInputs.push(input),
|
||||
) as never;
|
||||
},
|
||||
});
|
||||
@@ -215,7 +221,7 @@ async function runExecutor(
|
||||
} as never);
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
return { logs, meta, events, runtimeOptions, configOptions, sessionInputs, result };
|
||||
return { logs, meta, events, runtimeOptions, configOptions, sessionInputs, turnInputs, result };
|
||||
}
|
||||
|
||||
// Under `vi.useFakeTimers()`, setup before `ensureSession` still performs real
|
||||
@@ -603,7 +609,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");
|
||||
expect(prompt).not.toContain("PAPERCLIP_WAKE_PAYLOAD_JSON");
|
||||
expect(prompt).toContain("## Paperclip Wake Payload");
|
||||
expect(prompt).toContain("TEST-1");
|
||||
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");
|
||||
@@ -616,6 +624,57 @@ describe("shared ACPX engine runtime behavior", () => {
|
||||
expect(promptMetrics?.runtimeNoteChars).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("keeps large continuation history in ACP turns and preserves resume deltas", async () => {
|
||||
const root = await makeTempRoot();
|
||||
const config = {
|
||||
agent: "claude", cwd: root, stateDir: path.join(root, "state"), mode: "persistent",
|
||||
env: { PAPERCLIP_WAKE_PAYLOAD_JSON: "stale configured wake" },
|
||||
};
|
||||
const messages = Array.from({ length: 50 }, (_, index) => ({
|
||||
id: `message-${index}`, authorType: "user", authorId: "user-1",
|
||||
body: `Message ${index}: ${"context ".repeat(500)} End ${index}.`,
|
||||
createdAt: "2020-01-01T00:00:00.000Z", updatedAt: "2020-01-01T00:00:00.000Z",
|
||||
deleted: false, sourceTrust: { kind: "authenticated_user" },
|
||||
}));
|
||||
const completedActions = Array.from({ length: 50 }, (_, index) => ({
|
||||
runId: "prior-run", receiptId: `receipt-${index}`, operationId: `operation-${index}`,
|
||||
result: { text: `Completed action ${index}` },
|
||||
}));
|
||||
const continuation = {
|
||||
version: 1, companyId: "company-1", issueId: "issue-1",
|
||||
trigger: { reason: "issue_commented", interactionId: null, sourceRunId: null },
|
||||
originCommentIds: [messages[49]!.id], objective: "Preserve all context", messages,
|
||||
interactionOutcomes: [], unresolvedInteractionIds: [], completedWork: null,
|
||||
completedActions, coverage: { kind: "full_task_history", throughCommentId: messages[49]!.id, summaryThroughCommentId: null },
|
||||
};
|
||||
expect(Buffer.byteLength(JSON.stringify(continuation))).toBeGreaterThan(128 * 1024);
|
||||
const context = { taskId: "issue-1", paperclipWake: {
|
||||
reason: "issue_commented", issue: { id: "issue-1" }, executionContinuation: continuation,
|
||||
} };
|
||||
const fresh = await runExecutor(config, { context });
|
||||
const changedMessage = { ...messages[49]!, body: "Updated direction: preserve approval gates." };
|
||||
const resumed = await runExecutor(config, {
|
||||
runtime: { sessionParams: fresh.result.sessionParams },
|
||||
context: { ...context, paperclipWake: { ...context.paperclipWake, executionContinuation: {
|
||||
...continuation, resumeDelta: { baseRunId: "run-1", messages: [changedMessage] },
|
||||
} } },
|
||||
});
|
||||
expect(resumed.sessionInputs[0]?.resumeSessionId).toBe(fresh.result.sessionId);
|
||||
for (const run of [fresh, resumed]) {
|
||||
const sessionOptions = run.sessionInputs[0]?.sessionOptions as Record<string, unknown>;
|
||||
expect(sessionOptions.env).not.toHaveProperty("PAPERCLIP_WAKE_PAYLOAD_JSON");
|
||||
const prompt = String(run.turnInputs[0]?.text);
|
||||
expect(prompt).not.toContain("stale configured wake");
|
||||
for (const action of completedActions) expect(prompt).toContain(JSON.stringify(action));
|
||||
}
|
||||
const freshPrompt = String(fresh.turnInputs[0]?.text);
|
||||
for (const message of messages) expect(freshPrompt).toContain(JSON.stringify(message));
|
||||
const resumedPrompt = String(resumed.turnInputs[0]?.text);
|
||||
expect(resumedPrompt).toContain(JSON.stringify(changedMessage));
|
||||
expect(resumedPrompt).not.toContain(messages[0]!.body);
|
||||
expect(resumedPrompt).toContain('"kind":"task_history_delta"');
|
||||
});
|
||||
|
||||
it.each([
|
||||
["claude", false], ["codex", false], ["claude", true], ["codex", true],
|
||||
] as const)("keeps %s ACP conversation policy on fresh, resumed, and reset turns (custom=%s)", async (agent, custom) => {
|
||||
|
||||
@@ -72,7 +72,6 @@ import {
|
||||
removeMaintainerOnlySkillSymlinks,
|
||||
rewriteWorkspaceCwdEnvVarsForExecution,
|
||||
shapePaperclipWorkspaceEnvForExecution,
|
||||
stringifyPaperclipWakePayload,
|
||||
type PaperclipSkillEntry,
|
||||
} from "@paperclipai/adapter-utils/server-utils";
|
||||
import { shellQuote } from "@paperclipai/adapter-utils/ssh";
|
||||
@@ -1928,7 +1927,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;
|
||||
@@ -1937,7 +1935,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,
|
||||
|
||||
@@ -3717,6 +3717,16 @@ describe("refreshPaperclipWorkspaceEnvForExecution", () => {
|
||||
expect(env.PAPERCLIP_CLOUD_PROVIDER_TOKEN).toBe("cloud-token");
|
||||
});
|
||||
|
||||
it("does not restore the retired wake JSON variable from config", () => {
|
||||
const env: Record<string, string> = {};
|
||||
refreshPaperclipWorkspaceEnvForExecution({
|
||||
env,
|
||||
envConfig: { PAPERCLIP_WAKE_PAYLOAD_JSON: "stale wake" },
|
||||
workspaceCwd: null,
|
||||
});
|
||||
expect(env).not.toHaveProperty("PAPERCLIP_WAKE_PAYLOAD_JSON");
|
||||
});
|
||||
|
||||
it("never accepts PAPERCLIP_API_KEY from config env", () => {
|
||||
const env: Record<string, string> = {};
|
||||
|
||||
|
||||
@@ -162,10 +162,12 @@ export function isPaperclipRuntimeEnvKey(key: string): boolean {
|
||||
|
||||
// PAPERCLIP_API_KEY is never accepted from adapter/user config env: the
|
||||
// harness-minted run token is the only source of Paperclip API identity.
|
||||
// PAPERCLIP_WAKE_PAYLOAD_JSON is retired: wake context travels in the prompt,
|
||||
// and a configured copy can exceed OS process-launch limits.
|
||||
// Other PAPERCLIP_*-named config keys are allowed as long as Paperclip has
|
||||
// not assigned the same key for the run (runtime vars always win).
|
||||
export function isForbiddenConfigEnvKey(key: string): boolean {
|
||||
return key === "PAPERCLIP_API_KEY";
|
||||
return key === "PAPERCLIP_API_KEY" || key === "PAPERCLIP_WAKE_PAYLOAD_JSON";
|
||||
}
|
||||
const PAPERCLIP_SKILL_ROOT_RELATIVE_CANDIDATES = [
|
||||
"../../skills",
|
||||
@@ -1919,7 +1921,7 @@ export function stringifyPaperclipWakePayload(
|
||||
value: unknown,
|
||||
options: {
|
||||
// For prompt-embedded copies of the payload on lanes where another prompt
|
||||
// section already carries the issue description; the env-var copy should
|
||||
// section already carries the issue description. Other serialized copies
|
||||
// stay complete.
|
||||
omitIssueDescription?: boolean;
|
||||
} = {},
|
||||
|
||||
@@ -48,7 +48,6 @@ import {
|
||||
selectPaperclipTaskMarkdown,
|
||||
rewriteWorkspaceCwdEnvVarsForExecution,
|
||||
shapePaperclipWorkspaceEnvForExecution,
|
||||
stringifyPaperclipWakePayload,
|
||||
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
|
||||
DEFAULT_PAPERCLIP_CONVERSATION_PROMPT_TEMPLATE,
|
||||
} from "@paperclipai/adapter-utils/server-utils";
|
||||
@@ -241,7 +240,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) {
|
||||
@@ -265,9 +263,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,
|
||||
|
||||
@@ -47,7 +47,6 @@ import {
|
||||
renderPaperclipWakePrompt,
|
||||
selectPaperclipTaskMarkdown,
|
||||
isPaperclipRecoveryWakePayload,
|
||||
stringifyPaperclipWakePayload,
|
||||
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
|
||||
DEFAULT_PAPERCLIP_CONVERSATION_PROMPT_TEMPLATE,
|
||||
joinPromptSections,
|
||||
@@ -915,7 +914,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;
|
||||
@@ -938,9 +936,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,
|
||||
|
||||
@@ -168,6 +168,22 @@ describe("cursor_cloud execute", () => {
|
||||
expect(prompt).not.toContain("Create child issues");
|
||||
});
|
||||
|
||||
it("delivers a large wake through the SDK prompt without a configured JSON env copy", async () => {
|
||||
const sdkAgent = createMockSdkAgent();
|
||||
createMock.mockResolvedValue(sdkAgent);
|
||||
const ctx = createContext();
|
||||
const description = "start " + "context ".repeat(25_000) + " end";
|
||||
ctx.config.env = { CURSOR_API_KEY: "cursor-secret", PAPERCLIP_WAKE_PAYLOAD_JSON: description };
|
||||
ctx.context.paperclipWake = {
|
||||
reason: "issue_assigned",
|
||||
issue: { id: "issue-1", description },
|
||||
};
|
||||
const result = await execute(ctx);
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(createMock.mock.calls[0]?.[0]?.cloud?.envVars).not.toHaveProperty("PAPERCLIP_WAKE_PAYLOAD_JSON");
|
||||
expect(sdkAgent.send.mock.calls[0]?.[0]).toContain(description);
|
||||
});
|
||||
|
||||
it("creates a fresh Cursor agent and injects Paperclip env without CURSOR_API_KEY", async () => {
|
||||
const run = createMockRun({
|
||||
agentId: "agent-fresh",
|
||||
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
selectInitialCommunicationGuidance,
|
||||
isPaperclipRecoveryWakePayload,
|
||||
renderTemplate,
|
||||
stringifyPaperclipWakePayload,
|
||||
} from "@paperclipai/adapter-utils/server-utils";
|
||||
|
||||
type CursorCloudSession = {
|
||||
@@ -116,6 +115,8 @@ function buildWakeEnv(ctx: AdapterExecutionContext, configEnv: Record<string, st
|
||||
// PAPERCLIP_API_KEY is never accepted from config — the harness-minted run
|
||||
// token is the only source of Paperclip API identity.
|
||||
delete env.PAPERCLIP_API_KEY;
|
||||
// Wake context travels in the prompt; a configured copy can exceed spawn limits.
|
||||
delete env.PAPERCLIP_WAKE_PAYLOAD_JSON;
|
||||
|
||||
const wakeTaskId = trimNullable(context.taskId) ?? trimNullable(context.issueId);
|
||||
const wakeReason = trimNullable(context.wakeReason);
|
||||
@@ -125,7 +126,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;
|
||||
@@ -134,7 +134,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;
|
||||
|
||||
@@ -47,7 +47,6 @@ import {
|
||||
selectPaperclipTaskMarkdown,
|
||||
selectInitialCommunicationGuidance,
|
||||
isPaperclipRecoveryWakePayload,
|
||||
stringifyPaperclipWakePayload,
|
||||
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
|
||||
DEFAULT_PAPERCLIP_CONVERSATION_PROMPT_TEMPLATE,
|
||||
joinPromptSections,
|
||||
@@ -275,7 +274,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;
|
||||
@@ -298,9 +296,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,
|
||||
|
||||
@@ -50,7 +50,6 @@ import {
|
||||
selectPaperclipTaskMarkdown,
|
||||
selectInitialCommunicationGuidance,
|
||||
isPaperclipRecoveryWakePayload,
|
||||
stringifyPaperclipWakePayload,
|
||||
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
|
||||
DEFAULT_PAPERCLIP_CONVERSATION_PROMPT_TEMPLATE,
|
||||
runChildProcess,
|
||||
@@ -299,7 +298,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;
|
||||
@@ -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,
|
||||
|
||||
@@ -38,7 +38,6 @@ import {
|
||||
selectInitialCommunicationGuidance,
|
||||
isPaperclipRecoveryWakePayload,
|
||||
resolveLegacyPaperclipDesiredSkillNames,
|
||||
stringifyPaperclipWakePayload,
|
||||
refreshPaperclipWorkspaceEnvForExecution,
|
||||
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
|
||||
DEFAULT_PAPERCLIP_CONVERSATION_PROMPT_TEMPLATE,
|
||||
@@ -286,7 +285,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;
|
||||
@@ -295,7 +293,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,
|
||||
|
||||
@@ -104,6 +104,15 @@ describe("hermes-local adapter onSpawn forwarding", () => {
|
||||
expect(opts.onSpawn).toBe(onSpawn);
|
||||
});
|
||||
|
||||
it("keeps wake data in the prompt and drops configured JSON env copies", async () => {
|
||||
const { ctx } = makeCtx({ env: { PAPERCLIP_WAKE_PAYLOAD_JSON: "stale configured wake" } });
|
||||
const wake = { reason: "issue_assigned", issue: { id: "issue-1", description: "Current task brief" } };
|
||||
await execute({ ...ctx, context: { ...ctx.context, paperclipWake: wake } } as any);
|
||||
const call = vi.mocked(serverUtils.runChildProcess).mock.calls.at(-1)!;
|
||||
expect(call[3].env).not.toHaveProperty("PAPERCLIP_WAKE_PAYLOAD_JSON");
|
||||
expect(call[2]).toContainEqual(expect.stringContaining("Current task brief"));
|
||||
});
|
||||
|
||||
it("runChildProcess opts type includes onSpawn", () => {
|
||||
// Type-level assertion: if onSpawn were removed from the type,
|
||||
// this file would fail to compile. The runtime test above catches
|
||||
|
||||
@@ -499,6 +499,8 @@ export async function execute(
|
||||
// PAPERCLIP_API_KEY is never accepted from config — the harness-minted run
|
||||
// token is the only source of Paperclip API identity.
|
||||
delete env.PAPERCLIP_API_KEY;
|
||||
// Wake context travels in the prompt; drop both inherited and configured copies.
|
||||
delete env.PAPERCLIP_WAKE_PAYLOAD_JSON;
|
||||
if ((ctx as any).authToken) env.PAPERCLIP_API_KEY = (ctx as any).authToken;
|
||||
|
||||
// BUG FIX: Read task context from ctx.context (wake context), not ctx.config (adapter config)
|
||||
@@ -509,8 +511,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 =
|
||||
|
||||
@@ -42,7 +42,6 @@ import {
|
||||
selectPaperclipTaskMarkdown,
|
||||
selectInitialCommunicationGuidance,
|
||||
isPaperclipRecoveryWakePayload,
|
||||
stringifyPaperclipWakePayload,
|
||||
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
|
||||
DEFAULT_PAPERCLIP_CONVERSATION_PROMPT_TEMPLATE,
|
||||
} from "@paperclipai/adapter-utils/server-utils";
|
||||
@@ -272,7 +271,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;
|
||||
@@ -281,7 +279,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,
|
||||
|
||||
@@ -43,7 +43,6 @@ import {
|
||||
selectPaperclipTaskMarkdown,
|
||||
selectInitialCommunicationGuidance,
|
||||
isPaperclipRecoveryWakePayload,
|
||||
stringifyPaperclipWakePayload,
|
||||
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
|
||||
DEFAULT_PAPERCLIP_CONVERSATION_PROMPT_TEMPLATE,
|
||||
runChildProcess,
|
||||
@@ -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;
|
||||
if (issueWorkMode) env.PAPERCLIP_ISSUE_WORK_MODE = issueWorkMode;
|
||||
@@ -307,7 +305,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,
|
||||
|
||||
@@ -48,7 +48,6 @@ import {
|
||||
selectPaperclipTaskMarkdown,
|
||||
selectInitialCommunicationGuidance,
|
||||
isPaperclipRecoveryWakePayload,
|
||||
stringifyPaperclipWakePayload,
|
||||
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
|
||||
DEFAULT_PAPERCLIP_CONVERSATION_PROMPT_TEMPLATE,
|
||||
runChildProcess,
|
||||
@@ -303,7 +302,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;
|
||||
@@ -313,7 +311,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;
|
||||
|
||||
@@ -538,7 +538,7 @@ describe("codex execute", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("injects structured Paperclip wake payloads into env and prompt", async () => {
|
||||
it.each([false, true])("delivers oversized wake context through stdin (sandbox=%s)", async (sandbox) => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-codex-execute-wake-"));
|
||||
const workspace = path.join(root, "workspace");
|
||||
const commandPath = path.join(root, "codex");
|
||||
@@ -546,6 +546,8 @@ describe("codex execute", () => {
|
||||
await fs.mkdir(workspace, { recursive: true });
|
||||
await writeFakeCodexCommand(commandPath);
|
||||
|
||||
const description = "begin " + "full wake context ".repeat(16_384) + " end";
|
||||
expect(Buffer.byteLength(description)).toBeGreaterThan(128 * 1024);
|
||||
const previousHome = process.env.HOME;
|
||||
process.env.HOME = root;
|
||||
await seedSharedCodexAuth(root);
|
||||
@@ -572,6 +574,7 @@ describe("codex execute", () => {
|
||||
cwd: workspace,
|
||||
env: {
|
||||
PAPERCLIP_TEST_CAPTURE_PATH: capturePath,
|
||||
PAPERCLIP_WAKE_PAYLOAD_JSON: description,
|
||||
},
|
||||
promptTemplate: "Follow the paperclip heartbeat.",
|
||||
},
|
||||
@@ -585,7 +588,8 @@ describe("codex execute", () => {
|
||||
issue: {
|
||||
id: "issue-1",
|
||||
identifier: "PAP-874",
|
||||
title: "chat-speed issues",
|
||||
title: "Wake context test",
|
||||
description,
|
||||
status: "in_progress",
|
||||
priority: "medium",
|
||||
},
|
||||
@@ -618,6 +622,16 @@ describe("codex execute", () => {
|
||||
fallbackFetchNeeded: false,
|
||||
},
|
||||
},
|
||||
executionTarget: sandbox ? {
|
||||
kind: "remote",
|
||||
transport: "sandbox",
|
||||
providerKey: "test",
|
||||
environmentId: "env-1",
|
||||
leaseId: "lease-1",
|
||||
remoteCwd: workspace,
|
||||
timeoutMs: 30_000,
|
||||
runner: createLocalSandboxRunner(),
|
||||
} : undefined,
|
||||
authToken: "run-jwt-token",
|
||||
onLog: async () => {},
|
||||
});
|
||||
@@ -626,13 +640,10 @@ 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"],
|
||||
});
|
||||
expect(capture.paperclipEnvKeys).not.toContain("PAPERCLIP_WAKE_PAYLOAD_JSON");
|
||||
expect(capture.paperclipWakePayloadJson).toBeNull();
|
||||
expect(capture.prompt).toContain(description);
|
||||
expect(capture.prompt).toContain("- reason: issue_commented");
|
||||
expect(capture.prompt).toContain("## Paperclip Wake Payload");
|
||||
expect(capture.prompt).toContain("Use this wake to continue the task, applying new user direction and preserving its approval gates.");
|
||||
expect(capture.prompt).toContain("Do not switch to another issue until you have handled this wake.");
|
||||
@@ -1216,19 +1227,8 @@ 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: [],
|
||||
});
|
||||
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("- issue: PAP-1201 Fix gallery opening for inline images");
|
||||
|
||||
@@ -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 are under `/api`. Use JSON except for multipart attachment uploads and binary content downloads. 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.
|
||||
Adapters deliver the wake payload in the run prompt. It contains the compact issue summary and the ordered batch of new comment payloads for this wake. Read that prompt section 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.
|
||||
|
||||
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.
|
||||
|
||||
@@ -127,7 +127,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.
|
||||
If the run prompt includes a Paperclip wake payload, inspect that section 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.
|
||||
|
||||
Use comments incrementally:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user