mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-08 21:03:51 +02:00
fix(hermes): keep managed instructions out of resumed user turns (#15439)
Deliver managed instructions through Hermes's native system overlay while keeping current wake and runtime identity in user turns. Add fresh/resumed regression coverage and include Hermes in the default CI test roster. Fixes #15385 Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
3a726e676f
commit
abbd88007f
7 files changed
+295
-34
No files matched your search
@@ -312,6 +312,43 @@ up where the last one left off, maintaining conversation context,
|
||||
memories, and tool state across heartbeats. The `sessionCodec` validates
|
||||
and migrates session state between runs.
|
||||
|
||||
### Managed instructions and resumed turns
|
||||
|
||||
The local adapter reads `instructionsFilePath` on every run. It sends that
|
||||
bundle, its relative-reference base directory, and the standard Paperclip API
|
||||
guidance through Hermes's native `HERMES_EPHEMERAL_SYSTEM_PROMPT` overlay.
|
||||
Hermes applies this overlay at each model request, including after context
|
||||
compression. It does not append the overlay to saved user turns. This requires
|
||||
a Hermes CLI that supports the [documented environment hook](https://github.com/NousResearch/hermes-agent/blob/main/website/docs/reference/environment-variables.md).
|
||||
|
||||
Fresh sessions, resumed sessions, and session resets all receive the current
|
||||
bundle. Edits take effect on the next run; removing the bundle removes it from
|
||||
the next overlay. The server's session compatibility and conversation generation
|
||||
checks still control session reuse. A missing or failed resume remains a failed
|
||||
run and requires an explicit session reset; it does not automatically restart
|
||||
work. The next fresh run receives the complete current instruction overlay.
|
||||
|
||||
The `-q` user turn contains current runtime identity, task/wake context, handoff
|
||||
content, and the rendered custom `promptTemplate`, if configured. Custom templates
|
||||
run on every normal wake. `quiet` changes CLI display only. Disabling persistence
|
||||
uses full fresh-session context even if old session parameters are supplied.
|
||||
|
||||
An explicit `HERMES_EPHEMERAL_SYSTEM_PROMPT` in `env` is preserved before the
|
||||
Paperclip overlay. Hermes gives this environment hook precedence over its profile's
|
||||
configured personality/system prompt. Put required additional profile guidance in
|
||||
that explicit environment value or in the managed bundle. Paperclip does not write
|
||||
Hermes profile files or modify the parent process environment. Existing copies in
|
||||
old user turns remain in history until Hermes compresses them or the session resets.
|
||||
|
||||
Run the credential-free capture-process regression tests with:
|
||||
|
||||
```sh
|
||||
pnpm --filter @paperclipai/hermes-paperclip-adapter test -- src/server/execute.instructions.test.ts
|
||||
```
|
||||
|
||||
These tests verify actual child-process arguments and environment, not provider
|
||||
billing, model behavior, or measured token savings.
|
||||
|
||||
### Skills Integration
|
||||
|
||||
The adapter scans two skill sources and merges them:
|
||||
|
||||
@@ -114,6 +114,14 @@ tools, persistent memory, session persistence, skills, and MCP support.
|
||||
| env | object | {} | Extra environment variables |
|
||||
| promptTemplate | string | (default) | Custom prompt template with {{variable}} placeholders |
|
||||
|
||||
Managed \`instructionsFilePath\` bundles and standard Paperclip API guidance use
|
||||
Hermes's native \`HERMES_EPHEMERAL_SYSTEM_PROMPT\` overlay on every run, including
|
||||
resume and reset. They are applied at request time and are not appended to user
|
||||
history. The query keeps current runtime identity, wake/task content, and custom
|
||||
templates. Use a Hermes CLI that supports this environment hook. An explicit
|
||||
overlay in \`env\` is preserved; the hook takes precedence over Hermes's configured
|
||||
profile personality/system prompt.
|
||||
|
||||
## Hermes-Originated Paperclip Tasks
|
||||
|
||||
This adapter package also ships a Hermes-facing Paperclip task bridge skill:
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import type { AdapterExecutionContext } from "@paperclipai/adapter-utils";
|
||||
|
||||
import { execute } from "./execute.js";
|
||||
import { sessionCodec } from "./index.js";
|
||||
|
||||
const SESSION_ID = "20261006_200000_abcdef";
|
||||
const INSTRUCTIONS = "Synthetic governance marker: request approval before governed actions.";
|
||||
|
||||
interface Invocation {
|
||||
args: string[];
|
||||
systemPrompt: string | null;
|
||||
runId: string;
|
||||
}
|
||||
|
||||
describe("Hermes managed instruction delivery (real capture process)", () => {
|
||||
let dir: string;
|
||||
let command: string;
|
||||
let instructionsFilePath: string;
|
||||
let capturePath: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
dir = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-hermes-instructions-"));
|
||||
command = path.join(dir, "hermes.cjs");
|
||||
instructionsFilePath = path.join(dir, "instructions.md");
|
||||
capturePath = path.join(dir, "invocations.jsonl");
|
||||
await fs.writeFile(instructionsFilePath, INSTRUCTIONS);
|
||||
await fs.writeFile(command, `#!${process.execPath}
|
||||
const fs = require("node:fs");
|
||||
const args = process.argv.slice(2);
|
||||
fs.appendFileSync(process.env.CAPTURE_PATH, JSON.stringify({
|
||||
args,
|
||||
systemPrompt: process.env.HERMES_EPHEMERAL_SYSTEM_PROMPT ?? null,
|
||||
runId: process.env.PAPERCLIP_RUN_ID,
|
||||
}) + "\\n");
|
||||
if (process.env.FIXTURE_STDOUT) console.log(process.env.FIXTURE_STDOUT);
|
||||
if (args.includes("missing-session")) {
|
||||
console.error("\\u001b[1;31mSession missing-session not found.\\u001b[0m");
|
||||
process.exit(1);
|
||||
}
|
||||
if (process.env.FIXTURE_PROVIDER_ERROR) {
|
||||
console.error(process.env.FIXTURE_PROVIDER_ERROR);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log("Fixture completed\\nsession_id: ${SESSION_ID}");
|
||||
`, { mode: 0o700 });
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await fs.rm(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function context(overrides: Partial<AdapterExecutionContext> = {}): AdapterExecutionContext {
|
||||
return {
|
||||
runId: "run-fresh",
|
||||
agent: {
|
||||
id: "agent-synthetic", companyId: "company-synthetic", name: "Hermes fixture",
|
||||
adapterType: "hermes_local", adapterConfig: {},
|
||||
},
|
||||
runtime: { sessionId: null, sessionParams: null, sessionDisplayId: null, taskKey: "task-synthetic" },
|
||||
config: {
|
||||
hermesCommand: command, provider: "openrouter", model: "fixture-model", cwd: dir,
|
||||
instructionsFilePath, env: { CAPTURE_PATH: capturePath }, timeoutSec: 10, graceSec: 1,
|
||||
},
|
||||
context: {
|
||||
issueId: "task-synthetic",
|
||||
paperclipTaskMarkdownAssignment: "Complete current synthetic task.",
|
||||
paperclipTaskMarkdownCompact: "Current synthetic task.",
|
||||
paperclipWake: {
|
||||
reason: "issue_commented",
|
||||
issue: { id: "task-synthetic", title: "Synthetic task", status: "in_progress" },
|
||||
comments: [{ id: "comment-synthetic", body: "Fresh human direction." }],
|
||||
commentWindow: { requestedCount: 1, includedCount: 1, missingCount: 0 },
|
||||
fallbackFetchNeeded: false,
|
||||
},
|
||||
},
|
||||
onLog: async () => {},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
async function invocations(): Promise<Invocation[]> {
|
||||
return (await fs.readFile(capturePath, "utf8")).trim().split("\n").map((line) => JSON.parse(line));
|
||||
}
|
||||
|
||||
function query(invocation: Invocation): string {
|
||||
return invocation.args[invocation.args.indexOf("-q") + 1];
|
||||
}
|
||||
|
||||
it.each([false, true])("keeps instructions out of fresh and resumed user turns (conversation=%s)", async (conversationMode) => {
|
||||
const fresh = context();
|
||||
fresh.context.conversationMode = conversationMode;
|
||||
const first = await execute(fresh);
|
||||
const params = sessionCodec.deserialize(sessionCodec.serialize(first.sessionParams ?? null));
|
||||
expect(params?.sessionId).toBe(SESSION_ID);
|
||||
await execute({ ...fresh, runId: "run-resumed", runtime: { ...fresh.runtime, sessionParams: params } });
|
||||
|
||||
const [initial, resumed] = await invocations();
|
||||
expect(initial.args).not.toContain("--resume");
|
||||
expect(resumed.args.slice(resumed.args.indexOf("--resume"))).toEqual(["--resume", SESSION_ID]);
|
||||
for (const invocation of [initial, resumed]) {
|
||||
expect(invocation.systemPrompt).toContain(INSTRUCTIONS);
|
||||
expect(invocation.systemPrompt).toContain(`Resolve any relative file references from ${dir}/`);
|
||||
expect(query(invocation)).not.toContain(INSTRUCTIONS);
|
||||
expect(query(invocation)).not.toContain("Safe multiline update pattern:");
|
||||
expect(query(invocation)).toContain("Fresh human direction.");
|
||||
expect(query(invocation)).toContain(invocation.runId);
|
||||
expect(query(invocation)).toContain("company-synthetic");
|
||||
expect(invocation.systemPrompt).toContain("$PAPERCLIP_API_KEY");
|
||||
expect(invocation.systemPrompt).toContain('-H "X-Paperclip-Run-Id: $PAPERCLIP_RUN_ID"');
|
||||
expect(invocation.systemPrompt).toContain("body=$(cat <<'MD'");
|
||||
expect(invocation.systemPrompt).toContain("--data-binary @-");
|
||||
expect(invocation.systemPrompt).toContain('$api/issues/$PAPERCLIP_TASK_ID');
|
||||
expect(invocation.systemPrompt).not.toContain(invocation.runId);
|
||||
}
|
||||
});
|
||||
|
||||
it("reapplies current instructions after edits and a session reset", async () => {
|
||||
const ctx = context();
|
||||
await execute(ctx);
|
||||
await fs.writeFile(instructionsFilePath, "Updated governance marker.");
|
||||
await execute({ ...ctx, runtime: { ...ctx.runtime, sessionParams: { sessionId: SESSION_ID } } });
|
||||
await execute({ ...ctx, runId: "run-reset" });
|
||||
const [, edited, reset] = await invocations();
|
||||
for (const invocation of [edited, reset]) {
|
||||
expect(invocation.systemPrompt).toContain("Updated governance marker.");
|
||||
expect(invocation.systemPrompt).not.toContain(INSTRUCTIONS);
|
||||
expect(query(invocation)).not.toContain("Updated governance marker.");
|
||||
}
|
||||
expect(reset.args).not.toContain("--resume");
|
||||
});
|
||||
|
||||
it("uses fresh wake context when persistence is disabled despite stored session params", async () => {
|
||||
const ctx = context();
|
||||
const result = await execute({ ...ctx, config: { ...ctx.config, persistSession: false }, runtime: { ...ctx.runtime, sessionParams: { sessionId: SESSION_ID } } });
|
||||
const [invocation] = await invocations();
|
||||
expect(invocation.args).not.toContain("--resume");
|
||||
expect(query(invocation)).toContain("## Paperclip Wake Payload");
|
||||
expect(query(invocation)).not.toContain("## Paperclip Resume Delta");
|
||||
expect(invocation.systemPrompt).toContain(INSTRUCTIONS);
|
||||
expect(result.sessionParams).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps custom templates and quiet mode dynamic while preserving a configured system overlay", async () => {
|
||||
const ctx = context();
|
||||
await execute({ ...ctx, config: {
|
||||
...ctx.config, quiet: true, promptTemplate: "Current custom direction for {{runId}}.",
|
||||
env: { CAPTURE_PATH: capturePath, HERMES_EPHEMERAL_SYSTEM_PROMPT: "User configured system guidance." },
|
||||
}, runtime: { ...ctx.runtime, sessionParams: { sessionId: SESSION_ID } } });
|
||||
const [invocation] = await invocations();
|
||||
expect(invocation.args).toContain("-Q");
|
||||
expect(query(invocation)).toContain("Current custom direction for run-fresh.");
|
||||
expect(query(invocation)).not.toContain(INSTRUCTIONS);
|
||||
expect(invocation.systemPrompt).toContain("User configured system guidance.");
|
||||
expect(invocation.systemPrompt).toContain(INSTRUCTIONS);
|
||||
});
|
||||
|
||||
it.each([false, true])("keeps an empty wake runnable with current identity (conversation=%s)", async (conversationMode) => {
|
||||
await execute(context({ context: { conversationMode } }));
|
||||
const [invocation] = await invocations();
|
||||
expect(query(invocation)).toContain("run-fresh");
|
||||
expect(query(invocation)).not.toContain(INSTRUCTIONS);
|
||||
expect(invocation.systemPrompt).toContain(INSTRUCTIONS);
|
||||
});
|
||||
|
||||
it("surfaces a missing session without automatically restarting work", async () => {
|
||||
const ctx = context();
|
||||
const result = await execute({ ...ctx, runtime: { ...ctx.runtime, sessionParams: { sessionId: "missing-session" } } });
|
||||
const calls = await invocations();
|
||||
expect(calls).toHaveLength(1);
|
||||
expect(calls[0].args).toContain("--resume");
|
||||
expect(calls[0].systemPrompt).toContain(INSTRUCTIONS);
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.sessionParams).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not retry provider failures as a missing session", async () => {
|
||||
const ctx = context();
|
||||
const result = await execute({ ...ctx, config: { ...ctx.config, env: { CAPTURE_PATH: capturePath, FIXTURE_PROVIDER_ERROR: "Error: provider unavailable" } }, runtime: { ...ctx.runtime, sessionParams: { sessionId: SESSION_ID } } });
|
||||
expect(await invocations()).toHaveLength(1);
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.errorMessage).toBe("Error: provider unavailable");
|
||||
});
|
||||
|
||||
it("preserves failed work even when its output contains a missing-session diagnostic", async () => {
|
||||
const ctx = context();
|
||||
const result = await execute({ ...ctx, config: { ...ctx.config, env: {
|
||||
CAPTURE_PATH: capturePath,
|
||||
FIXTURE_STDOUT: `Already updated the task.\nSession ${SESSION_ID} not found.`,
|
||||
FIXTURE_PROVIDER_ERROR: "Error: provider unavailable after tool execution",
|
||||
} }, runtime: { ...ctx.runtime, sessionParams: { sessionId: SESSION_ID } } });
|
||||
expect(await invocations()).toHaveLength(1);
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.summary).toContain("Already updated the task.");
|
||||
expect(result.errorMessage).toBe("Error: provider unavailable after tool execution");
|
||||
});
|
||||
|
||||
it("removes an obsolete instruction overlay when the bundle is removed", async () => {
|
||||
const ctx = context();
|
||||
await execute(ctx);
|
||||
await execute({ ...ctx, config: { ...ctx.config, instructionsFilePath: undefined }, runtime: { ...ctx.runtime, sessionParams: { sessionId: SESSION_ID } } });
|
||||
const [, resumed] = await invocations();
|
||||
expect(resumed.systemPrompt).not.toContain(INSTRUCTIONS);
|
||||
expect(resumed.systemPrompt).toContain("Paperclip API guidance:");
|
||||
expect(query(resumed)).toContain("Fresh human direction.");
|
||||
});
|
||||
});
|
||||
@@ -34,7 +34,6 @@ import {
|
||||
renderTemplate,
|
||||
ensureAbsoluteDirectory,
|
||||
DEFAULT_PAPERCLIP_AGENT_PROMPT_TEMPLATE,
|
||||
DEFAULT_PAPERCLIP_CONVERSATION_PROMPT_TEMPLATE,
|
||||
joinPromptSections,
|
||||
selectPaperclipPromptSections,
|
||||
stringifyPaperclipWakePayload,
|
||||
@@ -82,7 +81,7 @@ export function resolveHermesCommand(config: Record<string, unknown>): string {
|
||||
// Wake-up prompt builder
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const HERMES_DEFAULT_PROMPT_TEMPLATE = [
|
||||
const HERMES_RUNTIME_IDENTITY_TEMPLATE = [
|
||||
'You are "{{agent.name}}", an AI agent employee in a Paperclip-managed company.',
|
||||
"",
|
||||
"Paperclip runtime identity:",
|
||||
@@ -90,7 +89,11 @@ const HERMES_DEFAULT_PROMPT_TEMPLATE = [
|
||||
"- Company ID: {{agent.companyId}}",
|
||||
"- Run ID: {{run.id}}",
|
||||
"- API base: {{paperclipApiUrl}}",
|
||||
"",
|
||||
].join("\n");
|
||||
|
||||
// Hermes applies this overlay at each API call, including after compaction,
|
||||
// without storing it in conversation history. Keep run/task deltas in -q.
|
||||
const HERMES_SYSTEM_PROMPT_TEMPLATE = [
|
||||
"Paperclip API guidance:",
|
||||
"- Use `curl` from the terminal for Paperclip API calls; browser/web extraction tools may not reach localhost.",
|
||||
"- Use `$PAPERCLIP_API_URL`, `$PAPERCLIP_API_KEY`, and `$PAPERCLIP_RUN_ID`; do not hard-code local ports or copy secrets into comments.",
|
||||
@@ -112,7 +115,7 @@ const HERMES_DEFAULT_PROMPT_TEMPLATE = [
|
||||
"MD",
|
||||
")",
|
||||
"jq -n --arg status done --arg comment \"$body\" '{status:$status, comment:$comment}' | \\",
|
||||
" curl -sS -X PATCH \"$api/issues/{{context.issueId}}\" \\",
|
||||
" curl -sS -X PATCH \"$api/issues/$PAPERCLIP_TASK_ID\" \\",
|
||||
" -H \"Authorization: Bearer $PAPERCLIP_API_KEY\" \\",
|
||||
" -H \"X-Paperclip-Run-Id: $PAPERCLIP_RUN_ID\" \\",
|
||||
" -H \"Content-Type: application/json\" \\",
|
||||
@@ -141,9 +144,7 @@ export function buildPrompt(
|
||||
options: { resumedSession?: boolean } = {},
|
||||
): string {
|
||||
const context = (ctx as any).context || {};
|
||||
const template = cfgString(config.promptTemplate) || (context.conversationMode === true
|
||||
? DEFAULT_PAPERCLIP_CONVERSATION_PROMPT_TEMPLATE
|
||||
: HERMES_DEFAULT_PROMPT_TEMPLATE);
|
||||
const template = cfgString(config.promptTemplate);
|
||||
const taskId = cfgString(context.taskId) || cfgString(context.issueId) || cfgString(ctx.config?.taskId);
|
||||
const taskTitle = cfgString(context.taskTitle) || cfgString(ctx.config?.taskTitle) || "";
|
||||
const taskBody = cfgString(context.taskBody) || cfgString(ctx.config?.taskBody) || "";
|
||||
@@ -200,10 +201,11 @@ export function buildPrompt(
|
||||
paperclipRunIdEnv: "PAPERCLIP_RUN_ID",
|
||||
};
|
||||
|
||||
const rendered = isPaperclipRecoveryWakePayload(context.paperclipWake)
|
||||
? ""
|
||||
: renderTemplate(renderConditionalSections(template, vars), vars);
|
||||
const rendered = template && !isPaperclipRecoveryWakePayload(context.paperclipWake)
|
||||
? renderTemplate(renderConditionalSections(template, vars), vars)
|
||||
: "";
|
||||
return joinPromptSections([
|
||||
renderTemplate(HERMES_RUNTIME_IDENTITY_TEMPLATE, vars),
|
||||
wakePrompt,
|
||||
sessionHandoffMarkdown,
|
||||
taskContextMarkdown,
|
||||
@@ -404,7 +406,7 @@ export async function execute(
|
||||
|
||||
// ── Load agent instructions file (Paperclip instruction bundles) ──────
|
||||
// Paperclip can materialize managed instructions into instructionsFilePath;
|
||||
// when present, inject that bundle into the Hermes prompt.
|
||||
// when present, reapply that bundle through Hermes's native system overlay.
|
||||
const instructionsFilePath = cfgString(config.instructionsFilePath);
|
||||
let agentInstructions = "";
|
||||
if (instructionsFilePath) {
|
||||
@@ -430,10 +432,8 @@ export async function execute(
|
||||
}
|
||||
|
||||
// ── Build prompt ───────────────────────────────────────────────────────
|
||||
let prompt = buildPrompt(ctx, config, { resumedSession: Boolean(prevSessionId) });
|
||||
if (agentInstructions) {
|
||||
prompt = agentInstructions + "\n\n---\n\n" + prompt;
|
||||
}
|
||||
const sessionId = persistSession ? prevSessionId : undefined;
|
||||
const prompt = buildPrompt(ctx, config, { resumedSession: Boolean(sessionId) });
|
||||
|
||||
// ── Build command args ─────────────────────────────────────────────────
|
||||
// Use -Q (quiet) to get clean output: just response + session_id line
|
||||
@@ -474,13 +474,9 @@ export async function execute(
|
||||
// system is designed for human-attended interactive sessions.
|
||||
args.push("--yolo");
|
||||
|
||||
if (persistSession && prevSessionId) {
|
||||
args.push("--resume", prevSessionId);
|
||||
}
|
||||
|
||||
if (extraArgs?.length) {
|
||||
args.push(...extraArgs);
|
||||
}
|
||||
// Failed resumes remain failures. Do not infer a safe restart from CLI text.
|
||||
if (sessionId) args.push("--resume", sessionId);
|
||||
if (extraArgs?.length) args.push(...extraArgs);
|
||||
|
||||
// ── Build environment ──────────────────────────────────────────────────
|
||||
const userEnv = config.env as Record<string, string> | undefined;
|
||||
@@ -491,6 +487,17 @@ export async function execute(
|
||||
...buildRuntimeToolsEnv(ctx.runtimeTools),
|
||||
};
|
||||
|
||||
// Scope the overlay to this child process. Read the current bundle on every
|
||||
// invocation; never infer its availability from a persisted session ID.
|
||||
// Preserve an operator's explicit overlay before Paperclip's instructions.
|
||||
env.HERMES_EPHEMERAL_SYSTEM_PROMPT = joinPromptSections([
|
||||
env.HERMES_EPHEMERAL_SYSTEM_PROMPT,
|
||||
agentInstructions,
|
||||
renderTemplate(HERMES_SYSTEM_PROMPT_TEMPLATE, {
|
||||
agent: ctx.agent,
|
||||
}),
|
||||
]);
|
||||
|
||||
if (ctx.runId) env.PAPERCLIP_RUN_ID = ctx.runId;
|
||||
|
||||
// PAPERCLIP_API_KEY is never accepted from config — the harness-minted run
|
||||
@@ -527,10 +534,10 @@ export async function execute(
|
||||
"stdout",
|
||||
`[hermes] Starting Hermes Agent (model=${model}, provider=${resolvedProvider} [${resolvedFrom}], timeout=${timeoutSec}s${maxTurns ? `, max_turns=${maxTurns}` : ""})\n`,
|
||||
);
|
||||
if (prevSessionId) {
|
||||
if (sessionId) {
|
||||
await ctx.onLog(
|
||||
"stdout",
|
||||
`[hermes] Resuming session: ${prevSessionId}\n`,
|
||||
`[hermes] Resuming session: ${sessionId}\n`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -234,19 +234,16 @@ test("keeps authoritative parent and ancestor context from task markdown", () =>
|
||||
expect(prompt).not.toContain("check the issue body or comments for references");
|
||||
});
|
||||
|
||||
test("renders safe Paperclip API examples from environment variables with multiline update preservation", () => {
|
||||
test("keeps current runtime identity in the user turn without repeating static API examples", () => {
|
||||
const prompt = buildPrompt(baseContext(), {
|
||||
paperclipApiUrl: "http://paperclip.local/api",
|
||||
});
|
||||
|
||||
expect(prompt).toContain("Use `$PAPERCLIP_API_URL`, `$PAPERCLIP_API_KEY`, and `$PAPERCLIP_RUN_ID`");
|
||||
expect(prompt).toContain("Displayed command logs may redact secrets");
|
||||
expect(prompt).toContain('-H "Authorization: Bearer $PAPERCLIP_API_KEY"');
|
||||
expect(prompt).toContain('-H "X-Paperclip-Run-Id: $PAPERCLIP_RUN_ID"');
|
||||
expect(prompt).toContain("body=$(cat <<'MD'");
|
||||
expect(prompt).toContain("jq -n --arg status done --arg comment \"$body\"");
|
||||
expect(prompt).toContain("--data-binary @-");
|
||||
expect(prompt).not.toContain("Authorization: Bearer <");
|
||||
expect(prompt).toContain("- Agent ID: agent-1");
|
||||
expect(prompt).toContain("- Company ID: company-1");
|
||||
expect(prompt).toContain("- Run ID: run-1");
|
||||
expect(prompt).toContain("- API base: http://paperclip.local/api");
|
||||
expect(prompt).not.toContain("Safe multiline update pattern:");
|
||||
});
|
||||
|
||||
test("preserves custom prompt templates while exposing runtime and wake variables", () => {
|
||||
@@ -276,7 +273,7 @@ test("preserves custom prompt templates while exposing runtime and wake variable
|
||||
expect(prompt).toContain('"reason":"issue_assigned"');
|
||||
expect(prompt).toContain("## Paperclip Wake Payload");
|
||||
expect(prompt).toContain("Issue description:\n```text\nUse the wake payload as runtime authority.\n```");
|
||||
expect(prompt).not.toContain("Paperclip runtime identity:");
|
||||
expect(prompt).toContain("Paperclip runtime identity:");
|
||||
});
|
||||
|
||||
test("keeps historical task markdown available to custom templates while automatic context uses assignment markdown", () => {
|
||||
|
||||
@@ -31,6 +31,7 @@ const nonServerProjects = [
|
||||
"@paperclipai/adapter-cursor-local",
|
||||
"@paperclipai/adapter-gemini-local",
|
||||
"@paperclipai/adapter-grok-local",
|
||||
"@paperclipai/hermes-paperclip-adapter",
|
||||
"@paperclipai/adapter-kimi-local",
|
||||
"@paperclipai/adapter-openclaw-gateway",
|
||||
"@paperclipai/adapter-opencode-local",
|
||||
|
||||
@@ -13,6 +13,7 @@ export default defineConfig({
|
||||
"packages/adapters/cursor-local",
|
||||
"packages/adapters/gemini-local",
|
||||
"packages/adapters/grok-local",
|
||||
"packages/adapters/hermes",
|
||||
"packages/adapters/kimi-local",
|
||||
"packages/adapters/openclaw-gateway",
|
||||
"packages/adapters/opencode-local",
|
||||
|
||||
Reference in new issue
Block a user