Clear legacy prompt templates for bundled agents

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-04-28 09:15:53 -05:00
1 parent 930b717ba5
commit e7d32f19cd
5 files changed
+79 -6

No files matched your search

@@ -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<string, unknown> }
| 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<string, unknown> } }
| undefined;
expect(approvalInput?.payload?.adapterConfig?.promptTemplate).toBeUndefined();
expect(approvalInput?.payload?.adapterConfig?.bootstrapPromptTemplate).toBeUndefined();
});
});
+10 -1
View File
@@ -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"
+2 -2
View File
@@ -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
@@ -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.
@@ -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/<adapter>.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)