diff --git a/packages/adapter-utils/src/server-utils.test.ts b/packages/adapter-utils/src/server-utils.test.ts index 06847445..f5e8f716 100644 --- a/packages/adapter-utils/src/server-utils.test.ts +++ b/packages/adapter-utils/src/server-utils.test.ts @@ -1,6 +1,7 @@ import { randomUUID } from "node:crypto"; import { describe, expect, it } from "vitest"; import { + applyPaperclipWorkspaceEnv, appendWithByteCap, DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE, renderPaperclipWakePrompt, @@ -425,6 +426,50 @@ describe("renderPaperclipWakePrompt", () => { }); }); +describe("applyPaperclipWorkspaceEnv", () => { + it("adds shared workspace env vars including AGENT_HOME", () => { + const env = applyPaperclipWorkspaceEnv( + {}, + { + workspaceCwd: "/tmp/workspace", + workspaceSource: "project_primary", + workspaceStrategy: "git_worktree", + workspaceId: "workspace-1", + workspaceRepoUrl: "https://github.com/paperclipai/paperclip.git", + workspaceRepoRef: "main", + workspaceBranch: "feature/test", + workspaceWorktreePath: "/tmp/worktree", + agentHome: "/tmp/agent-home", + }, + ); + + expect(env).toEqual({ + PAPERCLIP_WORKSPACE_CWD: "/tmp/workspace", + PAPERCLIP_WORKSPACE_SOURCE: "project_primary", + PAPERCLIP_WORKSPACE_STRATEGY: "git_worktree", + PAPERCLIP_WORKSPACE_ID: "workspace-1", + PAPERCLIP_WORKSPACE_REPO_URL: "https://github.com/paperclipai/paperclip.git", + PAPERCLIP_WORKSPACE_REPO_REF: "main", + PAPERCLIP_WORKSPACE_BRANCH: "feature/test", + PAPERCLIP_WORKSPACE_WORKTREE_PATH: "/tmp/worktree", + AGENT_HOME: "/tmp/agent-home", + }); + }); + + it("skips empty workspace env values", () => { + const env = applyPaperclipWorkspaceEnv( + {}, + { + workspaceCwd: "", + workspaceSource: null, + agentHome: "", + }, + ); + + expect(env).toEqual({}); + }); +}); + describe("appendWithByteCap", () => { it("keeps valid UTF-8 when trimming through multibyte text", () => { const output = appendWithByteCap("prefix ", "hello — world", 7); diff --git a/packages/adapter-utils/src/server-utils.ts b/packages/adapter-utils/src/server-utils.ts index 661010d3..425a1f3f 100644 --- a/packages/adapter-utils/src/server-utils.ts +++ b/packages/adapter-utils/src/server-utils.ts @@ -835,6 +835,41 @@ export function buildPaperclipEnv(agent: { id: string; companyId: string }): Rec return vars; } +export function applyPaperclipWorkspaceEnv( + env: Record, + input: { + workspaceCwd?: string | null; + workspaceSource?: string | null; + workspaceStrategy?: string | null; + workspaceId?: string | null; + workspaceRepoUrl?: string | null; + workspaceRepoRef?: string | null; + workspaceBranch?: string | null; + workspaceWorktreePath?: string | null; + agentHome?: string | null; + }, +): Record { + const mappings = [ + ["PAPERCLIP_WORKSPACE_CWD", input.workspaceCwd], + ["PAPERCLIP_WORKSPACE_SOURCE", input.workspaceSource], + ["PAPERCLIP_WORKSPACE_STRATEGY", input.workspaceStrategy], + ["PAPERCLIP_WORKSPACE_ID", input.workspaceId], + ["PAPERCLIP_WORKSPACE_REPO_URL", input.workspaceRepoUrl], + ["PAPERCLIP_WORKSPACE_REPO_REF", input.workspaceRepoRef], + ["PAPERCLIP_WORKSPACE_BRANCH", input.workspaceBranch], + ["PAPERCLIP_WORKSPACE_WORKTREE_PATH", input.workspaceWorktreePath], + ["AGENT_HOME", input.agentHome], + ] as const; + + for (const [key, value] of mappings) { + if (typeof value === "string" && value.length > 0) { + env[key] = value; + } + } + + return env; +} + export function sanitizeInheritedPaperclipEnv(baseEnv: NodeJS.ProcessEnv): NodeJS.ProcessEnv { const env: NodeJS.ProcessEnv = { ...baseEnv }; for (const key of Object.keys(env)) { diff --git a/packages/adapters/claude-local/src/server/execute.ts b/packages/adapters/claude-local/src/server/execute.ts index a79e7224..365295b6 100644 --- a/packages/adapters/claude-local/src/server/execute.ts +++ b/packages/adapters/claude-local/src/server/execute.ts @@ -24,6 +24,7 @@ import { asStringArray, parseObject, parseJson, + applyPaperclipWorkspaceEnv, buildPaperclipEnv, readPaperclipRuntimeSkillEntries, joinPromptSections, @@ -193,33 +194,17 @@ async function buildClaudeRuntimeConfig(input: ClaudeExecutionInput): Promise 0) { env.PAPERCLIP_WORKSPACES_JSON = JSON.stringify(workspaceHints); } diff --git a/packages/adapters/codex-local/src/server/execute.ts b/packages/adapters/codex-local/src/server/execute.ts index 2a6b253a..89f67079 100644 --- a/packages/adapters/codex-local/src/server/execute.ts +++ b/packages/adapters/codex-local/src/server/execute.ts @@ -19,6 +19,7 @@ import { asString, asNumber, parseObject, + applyPaperclipWorkspaceEnv, buildPaperclipEnv, buildInvocationEnvForLogs, ensureAbsoluteDirectory, @@ -421,33 +422,17 @@ export async function execute(ctx: AdapterExecutionContext): Promise 0) { env.PAPERCLIP_WORKSPACES_JSON = JSON.stringify(workspaceHints); } diff --git a/packages/adapters/cursor-local/src/server/execute.ts b/packages/adapters/cursor-local/src/server/execute.ts index 6d7f7966..5056b222 100644 --- a/packages/adapters/cursor-local/src/server/execute.ts +++ b/packages/adapters/cursor-local/src/server/execute.ts @@ -24,6 +24,7 @@ import { asNumber, asStringArray, parseObject, + applyPaperclipWorkspaceEnv, buildPaperclipEnv, buildInvocationEnvForLogs, ensureAbsoluteDirectory, @@ -277,24 +278,14 @@ export async function execute(ctx: AdapterExecutionContext): Promise 0) { env.PAPERCLIP_WORKSPACES_JSON = JSON.stringify(workspaceHints); } diff --git a/packages/adapters/gemini-local/src/server/execute.ts b/packages/adapters/gemini-local/src/server/execute.ts index 4c79b753..756e88fd 100644 --- a/packages/adapters/gemini-local/src/server/execute.ts +++ b/packages/adapters/gemini-local/src/server/execute.ts @@ -25,6 +25,7 @@ import { asNumber, asString, asStringArray, + applyPaperclipWorkspaceEnv, buildPaperclipEnv, buildInvocationEnvForLogs, ensureAbsoluteDirectory, @@ -240,12 +241,14 @@ export async function execute(ctx: AdapterExecutionContext): Promise 0) env.PAPERCLIP_LINKED_ISSUE_IDS = linkedIssueIds.join(","); if (wakePayloadJson) env.PAPERCLIP_WAKE_PAYLOAD_JSON = wakePayloadJson; - if (effectiveWorkspaceCwd) env.PAPERCLIP_WORKSPACE_CWD = effectiveWorkspaceCwd; - if (workspaceSource) env.PAPERCLIP_WORKSPACE_SOURCE = workspaceSource; - if (workspaceId) env.PAPERCLIP_WORKSPACE_ID = workspaceId; - if (workspaceRepoUrl) env.PAPERCLIP_WORKSPACE_REPO_URL = workspaceRepoUrl; - if (workspaceRepoRef) env.PAPERCLIP_WORKSPACE_REPO_REF = workspaceRepoRef; - if (agentHome) env.AGENT_HOME = agentHome; + applyPaperclipWorkspaceEnv(env, { + workspaceCwd: effectiveWorkspaceCwd, + workspaceSource, + workspaceId, + workspaceRepoUrl, + workspaceRepoRef, + agentHome, + }); if (workspaceHints.length > 0) env.PAPERCLIP_WORKSPACES_JSON = JSON.stringify(workspaceHints); const targetPaperclipApiUrl = adapterExecutionTargetPaperclipApiUrl(executionTarget); if (targetPaperclipApiUrl) env.PAPERCLIP_API_URL = targetPaperclipApiUrl; diff --git a/packages/adapters/opencode-local/src/server/execute.ts b/packages/adapters/opencode-local/src/server/execute.ts index 0aa86b17..fbb8d433 100644 --- a/packages/adapters/opencode-local/src/server/execute.ts +++ b/packages/adapters/opencode-local/src/server/execute.ts @@ -24,6 +24,7 @@ import { asNumber, asStringArray, parseObject, + applyPaperclipWorkspaceEnv, buildPaperclipEnv, joinPromptSections, buildInvocationEnvForLogs, @@ -199,12 +200,14 @@ export async function execute(ctx: AdapterExecutionContext): Promise 0) env.PAPERCLIP_LINKED_ISSUE_IDS = linkedIssueIds.join(","); if (wakePayloadJson) env.PAPERCLIP_WAKE_PAYLOAD_JSON = wakePayloadJson; - if (effectiveWorkspaceCwd) env.PAPERCLIP_WORKSPACE_CWD = effectiveWorkspaceCwd; - if (workspaceSource) env.PAPERCLIP_WORKSPACE_SOURCE = workspaceSource; - if (workspaceId) env.PAPERCLIP_WORKSPACE_ID = workspaceId; - if (workspaceRepoUrl) env.PAPERCLIP_WORKSPACE_REPO_URL = workspaceRepoUrl; - if (workspaceRepoRef) env.PAPERCLIP_WORKSPACE_REPO_REF = workspaceRepoRef; - if (agentHome) env.AGENT_HOME = agentHome; + applyPaperclipWorkspaceEnv(env, { + workspaceCwd: effectiveWorkspaceCwd, + workspaceSource, + workspaceId, + workspaceRepoUrl, + workspaceRepoRef, + agentHome, + }); if (workspaceHints.length > 0) env.PAPERCLIP_WORKSPACES_JSON = JSON.stringify(workspaceHints); const targetPaperclipApiUrl = adapterExecutionTargetPaperclipApiUrl(executionTarget); if (targetPaperclipApiUrl) env.PAPERCLIP_API_URL = targetPaperclipApiUrl; diff --git a/packages/adapters/pi-local/src/server/execute.ts b/packages/adapters/pi-local/src/server/execute.ts index 819be8e1..00b011a6 100644 --- a/packages/adapters/pi-local/src/server/execute.ts +++ b/packages/adapters/pi-local/src/server/execute.ts @@ -23,6 +23,7 @@ import { asNumber, asStringArray, parseObject, + applyPaperclipWorkspaceEnv, buildPaperclipEnv, joinPromptSections, buildInvocationEnvForLogs, @@ -228,12 +229,14 @@ export async function execute(ctx: AdapterExecutionContext): Promise 0) env.PAPERCLIP_LINKED_ISSUE_IDS = linkedIssueIds.join(","); if (wakePayloadJson) env.PAPERCLIP_WAKE_PAYLOAD_JSON = wakePayloadJson; - if (workspaceCwd) env.PAPERCLIP_WORKSPACE_CWD = workspaceCwd; - if (workspaceSource) env.PAPERCLIP_WORKSPACE_SOURCE = workspaceSource; - if (workspaceId) env.PAPERCLIP_WORKSPACE_ID = workspaceId; - if (workspaceRepoUrl) env.PAPERCLIP_WORKSPACE_REPO_URL = workspaceRepoUrl; - if (workspaceRepoRef) env.PAPERCLIP_WORKSPACE_REPO_REF = workspaceRepoRef; - if (agentHome) env.AGENT_HOME = agentHome; + applyPaperclipWorkspaceEnv(env, { + workspaceCwd, + workspaceSource, + workspaceId, + workspaceRepoUrl, + workspaceRepoRef, + agentHome, + }); if (workspaceHints.length > 0) env.PAPERCLIP_WORKSPACES_JSON = JSON.stringify(workspaceHints); const targetPaperclipApiUrl = adapterExecutionTargetPaperclipApiUrl(executionTarget); if (targetPaperclipApiUrl) env.PAPERCLIP_API_URL = targetPaperclipApiUrl; diff --git a/server/src/onboarding-assets/ceo/HEARTBEAT.md b/server/src/onboarding-assets/ceo/HEARTBEAT.md index 573dfea5..722e4414 100644 --- a/server/src/onboarding-assets/ceo/HEARTBEAT.md +++ b/server/src/onboarding-assets/ceo/HEARTBEAT.md @@ -9,7 +9,7 @@ Run this checklist on every heartbeat. This covers both your local planning/memo ## 2. Local Planning Check -1. Read today's plan from `./memory/YYYY-MM-DD.md` under "## Today's Plan". +1. Read today's plan from `$AGENT_HOME/memory/YYYY-MM-DD.md` under "## Today's Plan". 2. Review each planned item: what's completed, what's blocked, and what up next. 3. For any blockers, resolve them yourself or escalate to the board. 4. If you're ahead, start on the next highest priority. @@ -57,8 +57,8 @@ Status quick guide: ## 7. Fact Extraction 1. Check for new conversations since last extraction. -2. Extract durable facts to the relevant entity in `./life/` (PARA). -3. Update `./memory/YYYY-MM-DD.md` with timeline entries. +2. Extract durable facts to the relevant entity in `$AGENT_HOME/life/` (PARA). +3. Update `$AGENT_HOME/memory/YYYY-MM-DD.md` with timeline entries. 4. Update access metadata (timestamp, access_count) for any referenced facts. ## 8. Exit