feat: resolve agent workspace from session/project/fallback
Heartbeat service resolves cwd from task session, project primary workspace, or agent home directory (~/.paperclip/instances/.../workspaces/). Adapters receive workspace context and forward it as env vars and session params. cwd is now optional in adapter config. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -12,7 +12,7 @@ export const agentConfigurationDoc = `# claude_local agent configuration
|
||||
Adapter: claude_local
|
||||
|
||||
Core fields:
|
||||
- cwd (string, required): absolute working directory for the agent process
|
||||
- cwd (string, optional): default absolute working directory fallback for the agent process
|
||||
- model (string, optional): Claude model id
|
||||
- effort (string, optional): reasoning effort passed via --effort (low|medium|high)
|
||||
- promptTemplate (string, optional): run prompt template
|
||||
|
||||
@@ -63,6 +63,9 @@ interface ClaudeExecutionInput {
|
||||
interface ClaudeRuntimeConfig {
|
||||
command: string;
|
||||
cwd: string;
|
||||
workspaceId: string | null;
|
||||
workspaceRepoUrl: string | null;
|
||||
workspaceRepoRef: string | null;
|
||||
env: Record<string, string>;
|
||||
timeoutSec: number;
|
||||
graceSec: number;
|
||||
@@ -87,7 +90,13 @@ async function buildClaudeRuntimeConfig(input: ClaudeExecutionInput): Promise<Cl
|
||||
const { runId, agent, config, context, authToken } = input;
|
||||
|
||||
const command = asString(config.command, "claude");
|
||||
const cwd = asString(config.cwd, process.cwd());
|
||||
const workspaceContext = parseObject(context.paperclipWorkspace);
|
||||
const workspaceCwd = asString(workspaceContext.cwd, "");
|
||||
const workspaceSource = asString(workspaceContext.source, "");
|
||||
const workspaceId = asString(workspaceContext.workspaceId, "") || null;
|
||||
const workspaceRepoUrl = asString(workspaceContext.repoUrl, "") || null;
|
||||
const workspaceRepoRef = asString(workspaceContext.repoRef, "") || null;
|
||||
const cwd = workspaceCwd || asString(config.cwd, process.cwd());
|
||||
await ensureAbsoluteDirectory(cwd);
|
||||
|
||||
const envConfig = parseObject(config.env);
|
||||
@@ -138,6 +147,21 @@ async function buildClaudeRuntimeConfig(input: ClaudeExecutionInput): Promise<Cl
|
||||
if (linkedIssueIds.length > 0) {
|
||||
env.PAPERCLIP_LINKED_ISSUE_IDS = linkedIssueIds.join(",");
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(envConfig)) {
|
||||
if (typeof value === "string") env[key] = value;
|
||||
@@ -161,6 +185,9 @@ async function buildClaudeRuntimeConfig(input: ClaudeExecutionInput): Promise<Cl
|
||||
return {
|
||||
command,
|
||||
cwd,
|
||||
workspaceId,
|
||||
workspaceRepoUrl,
|
||||
workspaceRepoRef,
|
||||
env,
|
||||
timeoutSec,
|
||||
graceSec,
|
||||
@@ -225,7 +252,17 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
context,
|
||||
authToken,
|
||||
});
|
||||
const { command, cwd, env, timeoutSec, graceSec, extraArgs } = runtimeConfig;
|
||||
const {
|
||||
command,
|
||||
cwd,
|
||||
workspaceId,
|
||||
workspaceRepoUrl,
|
||||
workspaceRepoRef,
|
||||
env,
|
||||
timeoutSec,
|
||||
graceSec,
|
||||
extraArgs,
|
||||
} = runtimeConfig;
|
||||
const skillsDir = await buildSkillsDir();
|
||||
|
||||
const runtimeSessionParams = parseObject(runtime.sessionParams);
|
||||
@@ -372,7 +409,13 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
parsedStream.sessionId ??
|
||||
(asString(parsed.session_id, opts.fallbackSessionId ?? "") || opts.fallbackSessionId);
|
||||
const resolvedSessionParams = resolvedSessionId
|
||||
? ({ sessionId: resolvedSessionId, cwd } as Record<string, unknown>)
|
||||
? ({
|
||||
sessionId: resolvedSessionId,
|
||||
cwd,
|
||||
...(workspaceId ? { workspaceId } : {}),
|
||||
...(workspaceRepoUrl ? { repoUrl: workspaceRepoUrl } : {}),
|
||||
...(workspaceRepoRef ? { repoRef: workspaceRepoRef } : {}),
|
||||
} as Record<string, unknown>)
|
||||
: null;
|
||||
|
||||
return {
|
||||
|
||||
@@ -17,7 +17,16 @@ export const sessionCodec: AdapterSessionCodec = {
|
||||
readNonEmptyString(record.cwd) ??
|
||||
readNonEmptyString(record.workdir) ??
|
||||
readNonEmptyString(record.folder);
|
||||
return cwd ? { sessionId, cwd } : { sessionId };
|
||||
const workspaceId = readNonEmptyString(record.workspaceId) ?? readNonEmptyString(record.workspace_id);
|
||||
const repoUrl = readNonEmptyString(record.repoUrl) ?? readNonEmptyString(record.repo_url);
|
||||
const repoRef = readNonEmptyString(record.repoRef) ?? readNonEmptyString(record.repo_ref);
|
||||
return {
|
||||
sessionId,
|
||||
...(cwd ? { cwd } : {}),
|
||||
...(workspaceId ? { workspaceId } : {}),
|
||||
...(repoUrl ? { repoUrl } : {}),
|
||||
...(repoRef ? { repoRef } : {}),
|
||||
};
|
||||
},
|
||||
serialize(params: Record<string, unknown> | null) {
|
||||
if (!params) return null;
|
||||
@@ -27,7 +36,16 @@ export const sessionCodec: AdapterSessionCodec = {
|
||||
readNonEmptyString(params.cwd) ??
|
||||
readNonEmptyString(params.workdir) ??
|
||||
readNonEmptyString(params.folder);
|
||||
return cwd ? { sessionId, cwd } : { sessionId };
|
||||
const workspaceId = readNonEmptyString(params.workspaceId) ?? readNonEmptyString(params.workspace_id);
|
||||
const repoUrl = readNonEmptyString(params.repoUrl) ?? readNonEmptyString(params.repo_url);
|
||||
const repoRef = readNonEmptyString(params.repoRef) ?? readNonEmptyString(params.repo_ref);
|
||||
return {
|
||||
sessionId,
|
||||
...(cwd ? { cwd } : {}),
|
||||
...(workspaceId ? { workspaceId } : {}),
|
||||
...(repoUrl ? { repoUrl } : {}),
|
||||
...(repoRef ? { repoRef } : {}),
|
||||
};
|
||||
},
|
||||
getDisplayId(params: Record<string, unknown> | null) {
|
||||
if (!params) return null;
|
||||
|
||||
@@ -18,7 +18,7 @@ export const agentConfigurationDoc = `# codex_local agent configuration
|
||||
Adapter: codex_local
|
||||
|
||||
Core fields:
|
||||
- cwd (string, required): absolute working directory for the agent process
|
||||
- cwd (string, optional): default absolute working directory fallback for the agent process
|
||||
- model (string, optional): Codex model id
|
||||
- modelReasoningEffort (string, optional): reasoning effort override (minimal|low|medium|high) passed via -c model_reasoning_effort=...
|
||||
- promptTemplate (string, optional): run prompt template
|
||||
|
||||
@@ -108,7 +108,13 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
asBoolean(config.dangerouslyBypassSandbox, false),
|
||||
);
|
||||
|
||||
const cwd = asString(config.cwd, process.cwd());
|
||||
const workspaceContext = parseObject(context.paperclipWorkspace);
|
||||
const workspaceCwd = asString(workspaceContext.cwd, "");
|
||||
const workspaceSource = asString(workspaceContext.source, "");
|
||||
const workspaceId = asString(workspaceContext.workspaceId, "");
|
||||
const workspaceRepoUrl = asString(workspaceContext.repoUrl, "");
|
||||
const workspaceRepoRef = asString(workspaceContext.repoRef, "");
|
||||
const cwd = workspaceCwd || asString(config.cwd, process.cwd());
|
||||
await ensureAbsoluteDirectory(cwd);
|
||||
await ensureCodexSkillsInjected(onLog);
|
||||
const envConfig = parseObject(config.env);
|
||||
@@ -157,6 +163,21 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
if (linkedIssueIds.length > 0) {
|
||||
env.PAPERCLIP_LINKED_ISSUE_IDS = linkedIssueIds.join(",");
|
||||
}
|
||||
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;
|
||||
}
|
||||
for (const [k, v] of Object.entries(envConfig)) {
|
||||
if (typeof v === "string") env[k] = v;
|
||||
}
|
||||
@@ -270,7 +291,13 @@ export async function execute(ctx: AdapterExecutionContext): Promise<AdapterExec
|
||||
|
||||
const resolvedSessionId = attempt.parsed.sessionId ?? runtimeSessionId ?? runtime.sessionId ?? null;
|
||||
const resolvedSessionParams = resolvedSessionId
|
||||
? ({ sessionId: resolvedSessionId, cwd } as Record<string, unknown>)
|
||||
? ({
|
||||
sessionId: resolvedSessionId,
|
||||
cwd,
|
||||
...(workspaceId ? { workspaceId } : {}),
|
||||
...(workspaceRepoUrl ? { repoUrl: workspaceRepoUrl } : {}),
|
||||
...(workspaceRepoRef ? { repoRef: workspaceRepoRef } : {}),
|
||||
} as Record<string, unknown>)
|
||||
: null;
|
||||
const parsedError = typeof attempt.parsed.errorMessage === "string" ? attempt.parsed.errorMessage.trim() : "";
|
||||
const stderrLine = firstNonEmptyLine(attempt.proc.stderr);
|
||||
|
||||
@@ -17,7 +17,16 @@ export const sessionCodec: AdapterSessionCodec = {
|
||||
readNonEmptyString(record.cwd) ??
|
||||
readNonEmptyString(record.workdir) ??
|
||||
readNonEmptyString(record.folder);
|
||||
return cwd ? { sessionId, cwd } : { sessionId };
|
||||
const workspaceId = readNonEmptyString(record.workspaceId) ?? readNonEmptyString(record.workspace_id);
|
||||
const repoUrl = readNonEmptyString(record.repoUrl) ?? readNonEmptyString(record.repo_url);
|
||||
const repoRef = readNonEmptyString(record.repoRef) ?? readNonEmptyString(record.repo_ref);
|
||||
return {
|
||||
sessionId,
|
||||
...(cwd ? { cwd } : {}),
|
||||
...(workspaceId ? { workspaceId } : {}),
|
||||
...(repoUrl ? { repoUrl } : {}),
|
||||
...(repoRef ? { repoRef } : {}),
|
||||
};
|
||||
},
|
||||
serialize(params: Record<string, unknown> | null) {
|
||||
if (!params) return null;
|
||||
@@ -27,7 +36,16 @@ export const sessionCodec: AdapterSessionCodec = {
|
||||
readNonEmptyString(params.cwd) ??
|
||||
readNonEmptyString(params.workdir) ??
|
||||
readNonEmptyString(params.folder);
|
||||
return cwd ? { sessionId, cwd } : { sessionId };
|
||||
const workspaceId = readNonEmptyString(params.workspaceId) ?? readNonEmptyString(params.workspace_id);
|
||||
const repoUrl = readNonEmptyString(params.repoUrl) ?? readNonEmptyString(params.repo_url);
|
||||
const repoRef = readNonEmptyString(params.repoRef) ?? readNonEmptyString(params.repo_ref);
|
||||
return {
|
||||
sessionId,
|
||||
...(cwd ? { cwd } : {}),
|
||||
...(workspaceId ? { workspaceId } : {}),
|
||||
...(repoUrl ? { repoUrl } : {}),
|
||||
...(repoRef ? { repoRef } : {}),
|
||||
};
|
||||
},
|
||||
getDisplayId(params: Record<string, unknown> | null) {
|
||||
if (!params) return null;
|
||||
|
||||
Reference in New Issue
Block a user