Q116: a task without file access cannot write files, and its prompt says to use documents
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
938c8cad79
commit
d67996fd25
5 files changed
+29
-4
No files matched your search
@@ -14,6 +14,10 @@ const REVIEW_EXCERPT_CHARS = 4000;
|
||||
|
||||
export function writeDeliverableTool(deps: AgentToolDeps, ctx: AgentToolContext, args: JsonRecord): string {
|
||||
if (!ctx.workspace) throw new Error("No workspace is bound to this task; cannot write deliverables.");
|
||||
// Writing a file is writing the workspace: only a task the operator approved for file access may, as with run_command.
|
||||
if (!ctx.workspace.writeAccess) {
|
||||
throw new Error("This task has read-only workspace access, so it cannot write files. Put the output in a document (create_document), or ask for a task with requires_filesystem, which the operator approves.");
|
||||
}
|
||||
const path = requireString(args.path, "path");
|
||||
const content = requireString(args.content, "content");
|
||||
if (reviewsDeliverables(deps.db, ctx.orgId)) {
|
||||
|
||||
@@ -112,7 +112,7 @@ export const AGENT_TOOLS: ToolDefinition[] = [
|
||||
},
|
||||
{
|
||||
name: "write_deliverable",
|
||||
description: "Write a deliverable file into the company workspace folder (relative path). Use this to produce real output.",
|
||||
description: "Write a deliverable file into the company workspace folder (relative path). Only tasks with filesystem access may write files; otherwise use create_document.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
|
||||
@@ -267,7 +267,14 @@ export function buildTaskPrompt(input: {
|
||||
);
|
||||
return lines.join("\n");
|
||||
}
|
||||
if (input.task.requires_filesystem === 1 && input.workspace) {
|
||||
// Only a task with approved file access can write files; every other task delivers through documents.
|
||||
const writesFiles = input.task.requires_filesystem === 1 && input.workspace !== null;
|
||||
const planStep = writesFiles
|
||||
? "write the initial operating plan into the workspace (write_deliverable). Then use send_message with /handoff @Name and attachment_file_ids to pass the operating plan to the relevant report."
|
||||
: "write the initial operating plan as a document (create_document). Then point each relevant report to its doc_id in your message.";
|
||||
const outputStep = writesFiles ? "write real output with write_deliverable" : "write real output as documents (create_document)";
|
||||
const handoffStep = writesFiles ? " Hand files over with /handoff and attachment_file_ids." : " Point teammates to documents by doc_id.";
|
||||
if (writesFiles) {
|
||||
lines.push(
|
||||
"",
|
||||
"This task carries filesystem access: read the existing code with list_files and read_file before changing anything, write changes with write_deliverable, and prove they work with run_command (install, build, test). Real output is code that builds and tests that pass — a document describing the work does not close an engineering task.",
|
||||
@@ -296,14 +303,14 @@ export function buildTaskPrompt(input: {
|
||||
: `Tell your manager the plan in a direct message. Then ${park}`;
|
||||
lines.push(
|
||||
"",
|
||||
`Your team is currently empty. First ${firstHiresFor(input.agent.role_level)}, create and assign their first tasks (create_task), and write the initial operating plan into the workspace (write_deliverable). Then use send_message with /handoff @Name and attachment_file_ids to pass the operating plan to the relevant report. Brief EACH report in a direct message (send_message with to="Their Name") about the work you assigned them — note the thread_id each returns so you can follow up in the same conversation. ${announce}`,
|
||||
`Your team is currently empty. First ${firstHiresFor(input.agent.role_level)}, create and assign their first tasks (create_task), and ${planStep} Brief EACH report in a direct message (send_message with to="Their Name") about the work you assigned them — note the thread_id each returns so you can follow up in the same conversation. ${announce}`,
|
||||
);
|
||||
} else {
|
||||
const manager = input.managerName ?? "your manager";
|
||||
const managerReference = input.managerName ? `${manager} (${agentMention(input.managerName)})` : manager;
|
||||
lines.push(
|
||||
"",
|
||||
`First, read_messages to check if anyone has said something you should respond to — reply to them like a colleague, not a report. Then do the work: write real output with write_deliverable, raise decisions and blockers with ${managerReference} in a direct message (send_message with to=), and keep team-wide topics in channels. Hand files over with /handoff and attachment_file_ids. Keep conversation going — ask questions, share your thinking, push back if something seems wrong. Before you finish: if the work is genuinely done, call update_task_status(task_id="${input.task.id}", status="done", evidence="what you produced and where"); if it needs another session, move it to "in_progress" and say in the thread what remains — an open task will be run again.`,
|
||||
`First, read_messages to check if anyone has said something you should respond to — reply to them like a colleague, not a report. Then do the work: ${outputStep}, raise decisions and blockers with ${managerReference} in a direct message (send_message with to=), and keep team-wide topics in channels.${handoffStep} Keep conversation going — ask questions, share your thinking, push back if something seems wrong. Before you finish: if the work is genuinely done, call update_task_status(task_id="${input.task.id}", status="done", evidence="what you produced and where"); if it needs another session, move it to "in_progress" and say in the thread what remains — an open task will be run again.`,
|
||||
);
|
||||
}
|
||||
lines.push("", RECORD_LEARNINGS_INSTRUCTION);
|
||||
|
||||
@@ -226,6 +226,9 @@ describe("agent instruction wiring", () => {
|
||||
expect(capturedPrompt).toContain("Tell your manager the plan in a direct message.");
|
||||
expect(capturedPrompt).not.toContain("#general");
|
||||
expect(capturedPrompt).not.toContain("Then do the work: write real output");
|
||||
// A task without file access plans in a document; it is never told to write files.
|
||||
expect(capturedPrompt).toContain("write the initial operating plan as a document (create_document)");
|
||||
expect(capturedPrompt).not.toContain("(write_deliverable)");
|
||||
connection.close();
|
||||
});
|
||||
});
|
||||
@@ -191,6 +191,17 @@ describe("execution modes", () => {
|
||||
expect(db.prepare("select state from approvals where id = ?").get(review)).toEqual({ state: "pending" });
|
||||
});
|
||||
|
||||
test("a task without file access cannot write files, directly or by holding them for review", () => {
|
||||
for (const mode of ["ai_ceo", "ai_cofounder"] as const) {
|
||||
const h = harness(mode);
|
||||
const readOnly = { ...h.ctx, workspace: { ...h.ctx.workspace!, writeAccess: false } };
|
||||
expect(dispatchAgentTool(h.deps, readOnly, call("write_deliverable", { path: "notes.md", content: "x" })))
|
||||
.toBe("ERROR: This task has read-only workspace access, so it cannot write files. Put the output in a document (create_document), or ask for a task with requires_filesystem, which the operator approves.");
|
||||
expect(existsSync(join(h.ctx.workspace!.localPath, "notes.md"))).toBe(false);
|
||||
expect(h.connection.db.prepare("select count(*) as n from held_deliverables").get()).toEqual({ n: 0 });
|
||||
}
|
||||
});
|
||||
|
||||
test("AI CEO mode ships deliverables without review", () => {
|
||||
const h = harness("ai_ceo");
|
||||
const wrote = dispatchAgentTool(h.deps, h.ctx, call("write_deliverable", { path: "plan/business-plan.md", content: "# Plan" }));
|
||||
|
||||
Reference in new issue
Block a user