From e7d32f19cd740bbd99e9aed616979c691e40f1f2 Mon Sep 17 00:00:00 2001 From: Dotta Date: Tue, 28 Apr 2026 09:15:53 -0500 Subject: [PATCH] Clear legacy prompt templates for bundled agents Co-Authored-By: Paperclip --- .../src/__tests__/agent-skills-routes.test.ts | 64 +++++++++++++++++++ server/src/routes/agents.ts | 11 +++- skills/paperclip-create-agent/SKILL.md | 4 +- .../references/agent-instruction-templates.md | 2 +- .../references/draft-review-checklist.md | 4 +- 5 files changed, 79 insertions(+), 6 deletions(-) diff --git a/server/src/__tests__/agent-skills-routes.test.ts b/server/src/__tests__/agent-skills-routes.test.ts index dfa794f9edc..5bf6311a836 100644 --- a/server/src/__tests__/agent-skills-routes.test.ts +++ b/server/src/__tests__/agent-skills-routes.test.ts @@ -504,6 +504,37 @@ describe.sequential("agent skill routes", () => { }); }); + it("clears legacy prompt templates when directly created local agents already provide instructions", async () => { + const res = await requestApp(await createApp(), (baseUrl) => request(baseUrl) + .post("/api/companies/company-1/agents") + .send({ + name: "QA Agent", + role: "engineer", + adapterType: "claude_local", + adapterConfig: { + instructionsFilePath: "/tmp/existing/AGENTS.md", + promptTemplate: "You are QA.", + bootstrapPromptTemplate: "Bootstrap QA.", + }, + })); + + expect([200, 201], JSON.stringify(res.body)).toContain(res.status); + expect(mockAgentInstructionsService.materializeManagedBundle).not.toHaveBeenCalled(); + expect(mockAgentService.update).toHaveBeenCalledWith( + "11111111-1111-4111-8111-111111111111", + expect.objectContaining({ + adapterConfig: expect.objectContaining({ + instructionsFilePath: "/tmp/existing/AGENTS.md", + }), + }), + ); + const updateInput = mockAgentService.update.mock.calls.at(-1)?.[1] as + | { adapterConfig?: Record } + | undefined; + expect(updateInput?.adapterConfig?.promptTemplate).toBeUndefined(); + expect(updateInput?.adapterConfig?.bootstrapPromptTemplate).toBeUndefined(); + }); + it("materializes the bundled CEO instruction set for default CEO agents", async () => { const res = await requestApp(await createApp(), (baseUrl) => request(baseUrl) .post("/api/companies/company-1/agents") @@ -675,4 +706,37 @@ describe.sequential("agent skill routes", () => { | undefined; expect(approvalInput?.payload?.adapterConfig?.promptTemplate).toBeUndefined(); }); + + it("clears legacy prompt templates from hire approval payloads that already provide instructions", async () => { + const res = await request(await createApp(createDb(true))) + .post("/api/companies/company-1/agent-hires") + .send({ + name: "QA Agent", + role: "engineer", + adapterType: "claude_local", + adapterConfig: { + instructionsFilePath: "/tmp/existing/AGENTS.md", + promptTemplate: "You are QA.", + bootstrapPromptTemplate: "Bootstrap QA.", + }, + }); + + expect(res.status, JSON.stringify(res.body)).toBe(201); + expect(mockAgentInstructionsService.materializeManagedBundle).not.toHaveBeenCalled(); + expect(mockApprovalService.create).toHaveBeenCalledWith( + "company-1", + expect.objectContaining({ + payload: expect.objectContaining({ + adapterConfig: expect.objectContaining({ + instructionsFilePath: "/tmp/existing/AGENTS.md", + }), + }), + }), + ); + const approvalInput = mockApprovalService.create.mock.calls.at(-1)?.[1] as + | { payload?: { adapterConfig?: Record } } + | undefined; + expect(approvalInput?.payload?.adapterConfig?.promptTemplate).toBeUndefined(); + expect(approvalInput?.payload?.adapterConfig?.bootstrapPromptTemplate).toBeUndefined(); + }); }); diff --git a/server/src/routes/agents.ts b/server/src/routes/agents.ts index 766d0614b06..3a587755a44 100644 --- a/server/src/routes/agents.ts +++ b/server/src/routes/agents.ts @@ -703,7 +703,16 @@ export function agentRoutes( || Boolean(asNonEmptyString(adapterConfig.instructionsFilePath)) || Boolean(asNonEmptyString(adapterConfig.agentsMdPath)); if (hasExplicitInstructionsBundle) { - return agent; + const nextAdapterConfig = { ...adapterConfig }; + const hadLegacyPrompt = + Object.prototype.hasOwnProperty.call(nextAdapterConfig, "promptTemplate") + || Object.prototype.hasOwnProperty.call(nextAdapterConfig, "bootstrapPromptTemplate"); + delete nextAdapterConfig.promptTemplate; + delete nextAdapterConfig.bootstrapPromptTemplate; + if (!hadLegacyPrompt) return agent; + + const updated = await svc.update(agent.id, { adapterConfig: nextAdapterConfig }); + return (updated as T | null) ?? { ...agent, adapterConfig: nextAdapterConfig }; } const promptTemplate = typeof adapterConfig.promptTemplate === "string" diff --git a/skills/paperclip-create-agent/SKILL.md b/skills/paperclip-create-agent/SKILL.md index a5efd949989..6fc4539b3ef 100644 --- a/skills/paperclip-create-agent/SKILL.md +++ b/skills/paperclip-create-agent/SKILL.md @@ -83,9 +83,9 @@ curl -sS "$PAPERCLIP_API_URL/llms/agent-icons.txt" \ - leave timer heartbeats off by default; only set `runtimeConfig.heartbeat.enabled=true` with an `intervalSec` when the role genuinely needs scheduled recurring work or the user explicitly asked for it - if the role may handle private advisories or sensitive disclosures, confirm a confidential workflow exists first (dedicated skill or documented manual process) - capabilities -- run prompt in adapter config (`promptTemplate` where applicable) +- managed instructions bundle (`AGENTS.md`) for adapters that support it; avoid durable `promptTemplate` config - for coding or execution agents, include the Paperclip execution contract: start actionable work in the same heartbeat; do not stop at a plan unless planning was requested; leave durable progress with a clear next action; use child issues for long or parallel delegated work instead of polling; mark blocked work with owner/action; respect budget, pause/cancel, approval gates, and company boundaries -- instruction text such as `AGENTS.md` built from step 4; for local managed-bundle adapters, put the adapted `AGENTS.md` content in `adapterConfig.promptTemplate` unless you are a board user intentionally managing bundle paths/files +- instruction text such as `AGENTS.md` built from step 4; for local managed-bundle adapters, agent-authenticated hire requests may send the adapted `AGENTS.md` in `adapterConfig.promptTemplate` only as a creation-time transport because the server materializes it into managed `AGENTS.md` and removes the legacy field. Do not set `promptTemplate` when you are already setting an instructions bundle/path. - source issue linkage (`sourceIssueId` or `sourceIssueIds`) when this hire came from an issue ### 7. Review the draft against the quality checklist diff --git a/skills/paperclip-create-agent/references/agent-instruction-templates.md b/skills/paperclip-create-agent/references/agent-instruction-templates.md index e0a5bf5bb6b..3bb267ef729 100644 --- a/skills/paperclip-create-agent/references/agent-instruction-templates.md +++ b/skills/paperclip-create-agent/references/agent-instruction-templates.md @@ -41,7 +41,7 @@ If you are hiring a role that is not in this index, do not force a fit. Use the ## How to apply an exact template 1. Open the matching reference in `references/agents/`. -2. Copy that template into the new agent's instruction bundle (usually `AGENTS.md`). For hire requests using local managed-bundle adapters, set the adapted template as `adapterConfig.promptTemplate`; Paperclip materializes it into `AGENTS.md`. +2. Copy that template into the new agent's instruction bundle (usually `AGENTS.md`). For agent-authenticated hire requests using local managed-bundle adapters, `adapterConfig.promptTemplate` is only a creation-time transport for the adapted template; Paperclip materializes it into managed `AGENTS.md` and removes the legacy field. Do not combine `promptTemplate` with an explicit instructions bundle/path. 3. Replace placeholders like `{{companyName}}`, `{{managerTitle}}`, `{{issuePrefix}}`, and URLs. 4. Remove tools or workflows the target adapter cannot use. 5. Keep the Paperclip heartbeat requirement and the task-comment requirement. diff --git a/skills/paperclip-create-agent/references/draft-review-checklist.md b/skills/paperclip-create-agent/references/draft-review-checklist.md index 76dd35fcf70..18c097099ba 100644 --- a/skills/paperclip-create-agent/references/draft-review-checklist.md +++ b/skills/paperclip-create-agent/references/draft-review-checklist.md @@ -57,13 +57,13 @@ Use it for every path: exact template, adjacent template, or generic fallback. - [ ] `sourceIssueId` (or `sourceIssueIds`) is set when the hire was triggered by an issue - [ ] `desiredSkills` lists only skills that already exist in the company library, or will be installed first via the company-skills workflow - [ ] Adapter config matches this Paperclip instance (cwd, model, credentials) per `/llms/agent-configuration/.txt` -- [ ] `adapterConfig.promptTemplate` holds the adapted `AGENTS.md` for local managed-bundle adapters (unless you are intentionally managing bundle paths) +- [ ] Local managed-bundle adapters end up with managed `AGENTS.md` config, not durable `adapterConfig.promptTemplate`; use `promptTemplate` only as the creation-time transport when the API caller cannot set bundle files directly - [ ] Placeholders like `{{companyName}}`, `{{managerTitle}}`, `{{issuePrefix}}`, and any URL stubs are replaced with real values ## H. Safety and permissions (least privilege) - [ ] The hire grants only the access the role needs — no "just in case" permissions -- [ ] No secrets are embedded in plain text in `adapterConfig` or `promptTemplate` unless the adapter explicitly requires it; prefer environment-injected credentials or scoped skills +- [ ] No secrets are embedded in plain text in `adapterConfig`, creation-time instruction text, or any legacy prompt field; prefer environment-injected credentials or scoped skills - [ ] Any `desiredSkills` or adapter settings that expand external-system access, browser/network reach, filesystem scope, or secret-handling capability are individually justified in the hire comment - [ ] `runtimeConfig.heartbeat.enabled` is `false` unless the role genuinely needs scheduled recurring work AND `intervalSec` is justified in the hire comment - [ ] `AGENTS.md` explicitly names anything the role must never do (external posts, shared infra changes, destructive ops without approval)