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 {
|
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.");
|
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 path = requireString(args.path, "path");
|
||||||
const content = requireString(args.content, "content");
|
const content = requireString(args.content, "content");
|
||||||
if (reviewsDeliverables(deps.db, ctx.orgId)) {
|
if (reviewsDeliverables(deps.db, ctx.orgId)) {
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ export const AGENT_TOOLS: ToolDefinition[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "write_deliverable",
|
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: {
|
parameters: {
|
||||||
type: "object",
|
type: "object",
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
|
|||||||
@@ -267,7 +267,14 @@ export function buildTaskPrompt(input: {
|
|||||||
);
|
);
|
||||||
return lines.join("\n");
|
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(
|
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.",
|
"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}`;
|
: `Tell your manager the plan in a direct message. Then ${park}`;
|
||||||
lines.push(
|
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 {
|
} else {
|
||||||
const manager = input.managerName ?? "your manager";
|
const manager = input.managerName ?? "your manager";
|
||||||
const managerReference = input.managerName ? `${manager} (${agentMention(input.managerName)})` : manager;
|
const managerReference = input.managerName ? `${manager} (${agentMention(input.managerName)})` : manager;
|
||||||
lines.push(
|
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);
|
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).toContain("Tell your manager the plan in a direct message.");
|
||||||
expect(capturedPrompt).not.toContain("#general");
|
expect(capturedPrompt).not.toContain("#general");
|
||||||
expect(capturedPrompt).not.toContain("Then do the work: write real output");
|
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();
|
connection.close();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -191,6 +191,17 @@ describe("execution modes", () => {
|
|||||||
expect(db.prepare("select state from approvals where id = ?").get(review)).toEqual({ state: "pending" });
|
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", () => {
|
test("AI CEO mode ships deliverables without review", () => {
|
||||||
const h = harness("ai_ceo");
|
const h = harness("ai_ceo");
|
||||||
const wrote = dispatchAgentTool(h.deps, h.ctx, call("write_deliverable", { path: "plan/business-plan.md", content: "# Plan" }));
|
const wrote = dispatchAgentTool(h.deps, h.ctx, call("write_deliverable", { path: "plan/business-plan.md", content: "# Plan" }));
|
||||||
|
|||||||
Reference in new issue
Block a user