Q173: agents work in the company's or task's language, not the display language

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-09 04:40:52 -04:00
1 parent 538d834647
commit 3f14e3f706
36 files changed
+268 -105

No files matched your search

+8 -4
View File
@@ -80,6 +80,7 @@ test("the pinned CEO conversation is the Messages front door", async () => {
const editCompany = async (mode: string, autoHires: boolean) => {
await page.getByRole("button", { name: "Edit company" }).click();
await page.getByLabel("Who runs the work").selectOption(mode);
await expect(page.locator(".modal select[name=workLanguage]")).toHaveValue("en");
await page.getByLabel("Auto-approve hires").setChecked(autoHires);
await page.getByRole("button", { name: "Save" }).click();
};
@@ -138,14 +139,17 @@ test("the pinned CEO conversation is the Messages front door", async () => {
await taskDialog.getByRole("button", { name: "Edit" }).click();
await taskDialog.getByLabel("Writing").selectOption("technical");
await taskDialog.getByLabel("Requires workspace access").check();
// The working language is the task's own; blank names the company's.
await expect(taskDialog.locator("select[name=workLanguage] option").first()).toHaveText("Company default (English)");
await taskDialog.locator("select[name=workLanguage]").selectOption("zh");
await taskDialog.getByRole("button", { name: "Save" }).click();
await expect(taskDialog.getByRole("heading", { name: "Edit task" })).toHaveCount(0);
const edited = await page.evaluate(async () => {
const api = (window as unknown as { agentCompany: { loadSnapshot: () => Promise<{ ok: true; data: { tasks: Array<{ title: string; writingMode: string; requiresFilesystem: boolean }> } }> } }).agentCompany;
const api = (window as unknown as { agentCompany: { loadSnapshot: () => Promise<{ ok: true; data: { tasks: Array<{ title: string; writingMode: string; requiresFilesystem: boolean; workLanguage: string | null }> } }> } }).agentCompany;
const task = (await api.loadSnapshot()).data.tasks.find((item) => item.title === "Operationalize goal: Ship the FlowPilot MVP.")!;
return [task.writingMode, task.requiresFilesystem];
return [task.writingMode, task.requiresFilesystem, task.workLanguage];
});
expect(edited).toEqual(["technical", true]);
expect(edited).toEqual(["technical", true, "zh"]);
// A pending request opens for review: read it in full and decide with notes; nothing there pretends to edit it.
await taskDialog.getByRole("button", { name: "Close" }).click();
await page.evaluate(async () => {
@@ -378,7 +382,7 @@ test("the pinned CEO conversation is the Messages front door", async () => {
const child = (await api.loadSnapshot()).data.tasks.find((task) => task.title === "Child step")!;
await api.updateTask({
orgId: before.activeOrgId, taskId: child.id, title: "Child step", description: "d", assigneeAgentId: null, risk: "low", priority: "normal",
parentTaskId: parent.id, projectId: null, goalId: null, workspaceId: null, requiresFilesystem: false, writingMode: "conversational",
parentTaskId: parent.id, projectId: null, goalId: null, workspaceId: null, requiresFilesystem: false, writingMode: "conversational", workLanguage: null,
});
});
await page.getByRole("button", { name: "Work" }).click();
+1 -1
View File
@@ -94,7 +94,7 @@ describe("collaboration integration flow", () => {
revisionId = skills.importDirectory({ orgId, directoryPath: createSkillDirectory(), versionSemver: "1.0.0" });
skillId = (connection.db.prepare("select skill_id from skill_revisions where id = ?").get(revisionId) as { skill_id: string }).skill_id;
skills.bind({ orgId, agentId, skillId, revisionId });
skills.resolveRuntimeSnapshot({ orgId, agentId, runId: `run_${randomUUID().replaceAll("-", "")}`, maxTokens: 44_800, taskText: "", runModel: { providerKey: "openai", model: "gpt-5.4" }, houseStyle: true });
skills.resolveRuntimeSnapshot({ orgId, agentId, runId: `run_${randomUUID().replaceAll("-", "")}`, maxTokens: 44_800, taskText: "", runModel: { providerKey: "openai", model: "gpt-5.4" }, houseStyle: true, language: "en" });
});
const state = snapshot.load();
+14 -4
View File
@@ -173,11 +173,21 @@ describe("agent instruction wiring", () => {
expect(capturedInstructions).toContain("Connected external services you carry in this run: GitHub");
expect(capturedInstructions).toContain("list_connectors");
// Switching the app language redirects the whole company to Chinese.
const rerun = async () => {
connection.db.prepare("update task_runs set state = 'failed' where org_id = ?").run(orgId);
await execution.runTask({ orgId, taskId });
return capturedInstructions;
};
// The display language changes only what the operator sees.
organization.updateUiPreferences({ language: "zh" });
connection.db.prepare("update task_runs set state = 'failed' where org_id = ?").run(orgId);
await execution.runTask({ orgId, taskId });
expect(capturedInstructions).toContain("公司的工作语言是简体中文");
expect(await rerun()).toContain("The working language is English");
expect(capturedInstructions).not.toContain("公司的工作语言是简体中文");
// The company's working language redirects its agents, and a task can keep its own.
connection.db.prepare("update organizations set work_language = 'zh' where id = ?").run(orgId);
expect(await rerun()).toContain("公司的工作语言是简体中文");
connection.db.prepare("update tasks set work_language = 'en' where id = ?").run(taskId);
expect(await rerun()).toContain("The working language is English");
expect(capturedInstructions).not.toContain("公司的工作语言是简体中文");
});
test("a new department head's task prompt tells them to staff their department, not to work as an IC", async () => {
+17 -18
View File
@@ -171,7 +171,7 @@ describe("built-in skills", () => {
h.skills.bind({ orgId: h.orgId, agentId, skillId: skill.id, revisionId: skill.revision_id });
});
const prompt = (runId: string) => {
const id = h.connection.transaction(() => h.skills.resolveRuntimeSnapshot({ orgId: h.orgId, agentId, runId, maxTokens: SKILL_TOKENS, taskText: "", runModel: { providerKey: "openai", model: "gpt-5.4" }, houseStyle: true }));
const id = h.connection.transaction(() => h.skills.resolveRuntimeSnapshot({ orgId: h.orgId, agentId, runId, maxTokens: SKILL_TOKENS, taskText: "", runModel: { providerKey: "openai", model: "gpt-5.4" }, houseStyle: true, language: "en" }));
return (h.connection.db.prepare("select compiled_prompt from skill_runtime_snapshots where id = ?").get(id) as { compiled_prompt: string }).compiled_prompt;
};
expect(prompt("run_before")).toContain("# Skill: Backend Architect");
@@ -265,7 +265,7 @@ describe("built-in skills", () => {
expect(hired).toContain("Unknown skills skipped: no-such-skill");
const danaId = (h.connection.db.prepare("select id from agents where org_id = ? and name = 'Dana Lee'").get(h.orgId) as { id: string }).id;
const danaSnapshot = h.connection.transaction(() => h.skills.resolveRuntimeSnapshot({ orgId: h.orgId, agentId: danaId, runId: "run_dana", maxTokens: SKILL_TOKENS, taskText: "", runModel: { providerKey: "openai", model: "gpt-5.4" }, houseStyle: true }));
const danaSnapshot = h.connection.transaction(() => h.skills.resolveRuntimeSnapshot({ orgId: h.orgId, agentId: danaId, runId: "run_dana", maxTokens: SKILL_TOKENS, taskText: "", runModel: { providerKey: "openai", model: "gpt-5.4" }, houseStyle: true, language: "en" }));
const danaPrompt = (h.connection.db.prepare("select compiled_prompt from skill_runtime_snapshots where id = ?").get(danaSnapshot) as { compiled_prompt: string }).compiled_prompt;
expect(danaPrompt.indexOf("# Skill: humanizer")).toBe(0);
expect(danaPrompt).toContain("# Skill: Backend Architect");
@@ -274,15 +274,15 @@ describe("built-in skills", () => {
expect(danaPrompt.length).toBeLessThan(SKILL_TOKENS * 3 + 5_000);
// The CEO carries humanizer too - forced by resolution, not by binding.
expect(h.skills.hasRuntimeSkills(h.orgId, ceoId, true)).toBe(true);
const ceoSnapshot = h.connection.transaction(() => h.skills.resolveRuntimeSnapshot({ orgId: h.orgId, agentId: ceoId, runId: "run_ceo", maxTokens: SKILL_TOKENS, taskText: "", runModel: { providerKey: "openai", model: "gpt-5.4" }, houseStyle: true }));
expect(h.skills.hasRuntimeSkills(h.orgId, ceoId, true, "en")).toBe(true);
const ceoSnapshot = h.connection.transaction(() => h.skills.resolveRuntimeSnapshot({ orgId: h.orgId, agentId: ceoId, runId: "run_ceo", maxTokens: SKILL_TOKENS, taskText: "", runModel: { providerKey: "openai", model: "gpt-5.4" }, houseStyle: true, language: "en" }));
const ceoPrompt = (h.connection.db.prepare("select compiled_prompt from skill_runtime_snapshots where id = ?").get(ceoSnapshot) as { compiled_prompt: string }).compiled_prompt;
expect(ceoPrompt).toContain("# Skill: humanizer");
expect(ceoPrompt).not.toContain("# Skill: Backend Architect");
// Technical and structured work drops house style; a CEO without grants then carries nothing.
expect(h.skills.hasRuntimeSkills(h.orgId, ceoId, false)).toBe(false);
const technicalSnapshot = h.connection.transaction(() => h.skills.resolveRuntimeSnapshot({ orgId: h.orgId, agentId: danaId, runId: "run_dana_spec", maxTokens: SKILL_TOKENS, taskText: "", runModel: { providerKey: "openai", model: "gpt-5.4" }, houseStyle: false }));
expect(h.skills.hasRuntimeSkills(h.orgId, ceoId, false, "en")).toBe(false);
const technicalSnapshot = h.connection.transaction(() => h.skills.resolveRuntimeSnapshot({ orgId: h.orgId, agentId: danaId, runId: "run_dana_spec", maxTokens: SKILL_TOKENS, taskText: "", runModel: { providerKey: "openai", model: "gpt-5.4" }, houseStyle: false, language: "en" }));
const technicalPrompt = (h.connection.db.prepare("select compiled_prompt from skill_runtime_snapshots where id = ?").get(technicalSnapshot) as { compiled_prompt: string }).compiled_prompt;
expect(technicalPrompt).not.toContain("# Skill: humanizer");
expect(technicalPrompt.indexOf("# Skill: Backend Architect")).toBe(0);
@@ -295,7 +295,7 @@ describe("built-in skills", () => {
h.connection.close();
});
test("the forced humanizer follows the app language, with cross-language fallback", () => {
test("the forced humanizer follows the working language, with cross-language fallback", () => {
const h = harness();
const staged = stageBuiltinSkillsLibrary(h.sourceRoot, h.libraryRoot);
h.connection.transaction(() => importStagedSkills(h.skills, h.orgId, staged));
@@ -306,28 +306,27 @@ describe("built-in skills", () => {
modelProviderId: null, modelId: null, budgetCents: 1000, managerAgentId: null,
});
});
const promptFor = (runId: string) => {
const snapshotId = h.connection.transaction(() => h.skills.resolveRuntimeSnapshot({ orgId: h.orgId, agentId, runId, maxTokens: SKILL_TOKENS, taskText: "", runModel: { providerKey: "openai", model: "gpt-5.4" }, houseStyle: true }));
const promptFor = (runId: string, language: "en" | "zh") => {
const snapshotId = h.connection.transaction(() => h.skills.resolveRuntimeSnapshot({ orgId: h.orgId, agentId, runId, maxTokens: SKILL_TOKENS, taskText: "", runModel: { providerKey: "openai", model: "gpt-5.4" }, houseStyle: true, language }));
return (h.connection.db.prepare("select compiled_prompt from skill_runtime_snapshots where id = ?").get(snapshotId) as { compiled_prompt: string }).compiled_prompt;
};
// English UI (default): the English humanizer, and only that one.
const englishPrompt = promptFor("run_lang_en");
// English work: the English humanizer, and only that one.
const englishPrompt = promptFor("run_lang_en", "en");
expect(englishPrompt).toContain("# Skill: humanizer");
expect(englishPrompt).not.toContain("humanizer-zh");
// Chinese UI: the Chinese variant replaces it.
h.connection.transaction(() => h.organization.updateUiPreferences({ language: "zh" }));
const chinesePrompt = promptFor("run_lang_zh");
// Chinese work: the Chinese variant replaces it.
const chinesePrompt = promptFor("run_lang_zh", "zh");
expect(chinesePrompt).toContain("# Skill: humanizer-zh");
expect(chinesePrompt).not.toContain("# Skill: humanizer\n");
// Chinese UI against a catalogue without the zh variant falls back to
// Chinese work against a catalogue without the zh variant falls back to
// English house style instead of silently dropping it.
h.connection.transaction(() => {
h.connection.db.prepare("update skill_definitions set status = 'retired' where org_id = ? and slug = 'humanizer-zh'").run(h.orgId);
});
const fallbackPrompt = promptFor("run_lang_fallback");
const fallbackPrompt = promptFor("run_lang_fallback", "zh");
expect(fallbackPrompt).toContain("# Skill: humanizer");
h.connection.close();
});
@@ -353,7 +352,7 @@ describe("built-in skills", () => {
modelProviderId: null, modelId: null, budgetCents: 1000, managerAgentId: null,
});
});
const snapshotId = h.connection.transaction(() => h.skills.resolveRuntimeSnapshot({ orgId: h.orgId, agentId, runId: "run_tiny", maxTokens: 10, taskText: "", runModel: { providerKey: "openai", model: "gpt-5.4" }, houseStyle: true }));
const snapshotId = h.connection.transaction(() => h.skills.resolveRuntimeSnapshot({ orgId: h.orgId, agentId, runId: "run_tiny", maxTokens: 10, taskText: "", runModel: { providerKey: "openai", model: "gpt-5.4" }, houseStyle: true, language: "en" }));
const prompt = (h.connection.db.prepare("select compiled_prompt from skill_runtime_snapshots where id = ?").get(snapshotId) as { compiled_prompt: string }).compiled_prompt;
expect(prompt).not.toContain("# Skill:");
expect(prompt).toContain("# Skills omitted for size");
@@ -380,7 +379,7 @@ describe("built-in skills", () => {
bindSkillSlugs({ db: h.connection.db, organization: h.organization, chat: new ChatService(h.connection.db) }, h.orgId, agentId, ["marketing-growth-hacker", "engineering-backend-architect"]);
});
const compile = (runId: string, maxTokens: number, taskText: string) => {
const snapshotId = h.connection.transaction(() => h.skills.resolveRuntimeSnapshot({ orgId: h.orgId, agentId, runId, maxTokens, taskText, runModel: { providerKey: "openai", model: "gpt-5.4" }, houseStyle: true }));
const snapshotId = h.connection.transaction(() => h.skills.resolveRuntimeSnapshot({ orgId: h.orgId, agentId, runId, maxTokens, taskText, runModel: { providerKey: "openai", model: "gpt-5.4" }, houseStyle: true, language: "en" }));
return (h.connection.db.prepare("select compiled_prompt from skill_runtime_snapshots where id = ?").get(snapshotId) as { compiled_prompt: string }).compiled_prompt;
};
const parts = compile("run_full", 1_000_000, "").split("\n\n---\n\n");
+4 -4
View File
@@ -35,7 +35,7 @@ describe("domain services", () => {
const connection = createConnection();
const rows = connection.db.prepare("select version from schema_migrations order by version asc").all() as Array<{ version: number }>;
expect(rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103]);
expect(rows.map((row) => row.version)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104]);
connection.close();
});
@@ -193,7 +193,7 @@ describe("domain services", () => {
skills.bind({ orgId, agentId, skillId: skill.id, revisionId });
rmSync(skillDir, { recursive: true, force: true });
skills.runTests({ orgId, revisionId });
skills.resolveRuntimeSnapshot({ orgId, agentId, runId: "run-review-1", maxTokens: 44_800, taskText: "", runModel: { providerKey: "openai", model: "gpt-5.4" }, houseStyle: true });
skills.resolveRuntimeSnapshot({ orgId, agentId, runId: "run-review-1", maxTokens: 44_800, taskText: "", runModel: { providerKey: "openai", model: "gpt-5.4" }, houseStyle: true, language: "en" });
});
// Catalogue metadata ships in the snapshot; skill internals stay in the
@@ -254,10 +254,10 @@ describe("domain services", () => {
.get(orgA, "review-routes") as { id: string }).id;
expect(() => skills.bind({ orgId: orgA, agentId: agentB, skillId, revisionId })).toThrow("Agent was not found.");
skills.bind({ orgId: orgA, agentId: agentA, skillId, revisionId });
const firstResolve = skills.resolveRuntimeSnapshot({ orgId: orgA, agentId: agentA, runId: "run-review-1", maxTokens: 44_800, taskText: "", runModel: { providerKey: "openai", model: "gpt-5.4" }, houseStyle: true });
const firstResolve = skills.resolveRuntimeSnapshot({ orgId: orgA, agentId: agentA, runId: "run-review-1", maxTokens: 44_800, taskText: "", runModel: { providerKey: "openai", model: "gpt-5.4" }, houseStyle: true, language: "en" });
// Idempotent per run: a recovered run reuses its frozen snapshot
// instead of failing on the uniqueness guard.
expect(skills.resolveRuntimeSnapshot({ orgId: orgA, agentId: agentA, runId: "run-review-1", maxTokens: 44_800, taskText: "", runModel: { providerKey: "openai", model: "gpt-5.4" }, houseStyle: true })).toBe(firstResolve);
expect(skills.resolveRuntimeSnapshot({ orgId: orgA, agentId: agentA, runId: "run-review-1", maxTokens: 44_800, taskText: "", runModel: { providerKey: "openai", model: "gpt-5.4" }, houseStyle: true, language: "en" })).toBe(firstResolve);
expect(connection.db.prepare("select count(*) as n from skill_runtime_snapshots where run_id = 'run-review-1'").get())
.toEqual({ n: 1 });
});
+1 -1
View File
@@ -315,7 +315,7 @@ describe("execution modes", () => {
}));
const task = () => db.prepare("select * from tasks where id = ?").get(taskId) as unknown as Parameters<typeof executionApprovalProblem>[2];
const taskApprovals = () => db.prepare("select id, state, details from approvals where source_entity_id = ? order by rowid").all(taskId) as Array<{ id: string; state: string; details: string }>;
const edit = (description: string) => h.connection.transaction(() => h.deps.organization.updateTask({
const edit = (description: string) => h.connection.transaction(() => h.deps.organization.updateTask({ workLanguage: null,
orgId: h.orgId, taskId, title: "Wipe staging", description, assigneeAgentId: h.ceoId, risk: "high", priority: TASK_PRIORITY.NORMAL, parentTaskId: null,
...currentLinks(db, taskId),
}));
+2 -2
View File
@@ -15,7 +15,7 @@ describe("company settings after onboarding", () => {
});
const update = (change: Partial<{ name: string; executionMode: "ai_ceo" | "ai_cofounder" }>) => connection.transaction(() => organization.updateOrganization({
orgId, name: "Acme Labs", description: "Analytics for CSMs.", mission: "Keep customers.", goal: "Ship the MVP.",
executionMode: "ai_cofounder", autoApproveHires: false, reviewDeliverables: true, ...change,
executionMode: "ai_cofounder", autoApproveHires: false, reviewDeliverables: true, workLanguage: "en", ...change,
}));
update({});
expect(connection.db.prepare("select name, description, mission, goal, execution_mode, auto_approve_hires, review_deliverables from organizations where id = ?").get(orgId))
@@ -23,7 +23,7 @@ describe("company settings after onboarding", () => {
const audit = connection.db.prepare("select payload_json from audit_events where entity_type = 'organization' and action = 'updated' order by rowid desc limit 1").get() as { payload_json: string };
expect(JSON.parse(audit.payload_json).changed).toEqual(expect.arrayContaining(["name", "executionMode", "reviewDeliverables"]));
expect(() => update({ name: " " })).toThrow("The company needs a name.");
expect(() => organization.updateOrganization({ orgId: "org_missing", name: "X", description: "", mission: "", goal: "", executionMode: "ai_ceo", autoApproveHires: true, reviewDeliverables: false }))
expect(() => organization.updateOrganization({ orgId: "org_missing", name: "X", description: "", mission: "", goal: "", executionMode: "ai_ceo", autoApproveHires: true, reviewDeliverables: false, workLanguage: "en" }))
.toThrow("Organization was not found.");
connection.close();
});
+1 -1
View File
@@ -42,7 +42,7 @@ describe("skill compatibility", () => {
skills.bind({ orgId, agentId, skillId: skill.id, revisionId });
});
const prompt = (runId: string, providerKey: string, model: string) => {
const id = connection.transaction(() => skills.resolveRuntimeSnapshot({ orgId, agentId, runId, maxTokens: 40_000, taskText: "", runModel: { providerKey, model }, houseStyle: true }));
const id = connection.transaction(() => skills.resolveRuntimeSnapshot({ orgId, agentId, runId, maxTokens: 40_000, taskText: "", runModel: { providerKey, model }, houseStyle: true, language: "en" }));
return connection.db.prepare("select compiled_prompt, source_revision_ids from skill_runtime_snapshots where id = ?").get(id) as { compiled_prompt: string; source_revision_ids: string };
};
+10 -10
View File
@@ -98,10 +98,10 @@ describe("skill grants", () => {
expect(listSkills?.description).not.toContain("every skill automatically");
expect(listSkills?.description).toContain("the CEO included");
const skills = new SkillService(h.connection.db, "");
expect(skills.hasRuntimeSkills(h.orgId, h.ceoId, true)).toBe(false);
expect(skills.hasRuntimeSkills(h.orgId, h.ceoId, true, "en")).toBe(false);
expect(dispatchAgentTool(h.deps, h.ceoCtx, call("grant_skill", { agent_name: "CEO", skill: "release-gate" })))
.toBe("CEO now has the Release Gate skill; it joins their next run.");
expect(skills.hasRuntimeSkills(h.orgId, h.ceoId, true)).toBe(true);
expect(skills.hasRuntimeSkills(h.orgId, h.ceoId, true, "en")).toBe(true);
h.connection.close();
});
@@ -116,11 +116,11 @@ describe("skill grants", () => {
expect(listing).toContain("Release Gate (release-gate): granted to Dana");
const skills = new SkillService(h.connection.db, "");
expect(skills.hasRuntimeSkills(h.orgId, h.danaId, true)).toBe(true);
expect(skills.hasRuntimeSkills(h.orgId, h.danaId, true, "en")).toBe(true);
const revoked = dispatchAgentTool(h.deps, h.ceoCtx, call("revoke_skill", { agent_name: "Dana", skill: "release-gate" }));
expect(revoked).toContain("no longer has the Release Gate skill");
expect(skills.hasRuntimeSkills(h.orgId, h.danaId, true)).toBe(false);
expect(skills.hasRuntimeSkills(h.orgId, h.danaId, true, "en")).toBe(false);
});
test("staff cannot hand out skills", () => {
@@ -128,7 +128,7 @@ describe("skill grants", () => {
importSkill(h, "Release Gate", "release-gate");
const denied = dispatchAgentTool(h.deps, h.danaCtx, call("grant_skill", { agent_name: "Dana", skill: "release-gate" }));
expect(denied).toContain("ERROR");
expect(new SkillService(h.connection.db, "").hasRuntimeSkills(h.orgId, h.danaId, true)).toBe(false);
expect(new SkillService(h.connection.db, "").hasRuntimeSkills(h.orgId, h.danaId, true, "en")).toBe(false);
});
test("the humanizer house-style skill reaches every agent without bindings and cannot be revoked", () => {
@@ -136,18 +136,18 @@ describe("skill grants", () => {
const skills = new SkillService(h.connection.db, "");
// A non-forced skill grants nothing on its own - not even to the CEO.
importSkill(h, "Release Gate", "release-gate");
expect(skills.hasRuntimeSkills(h.orgId, h.ceoId, true)).toBe(false);
expect(skills.hasRuntimeSkills(h.orgId, h.danaId, true)).toBe(false);
expect(skills.hasRuntimeSkills(h.orgId, h.ceoId, true, "en")).toBe(false);
expect(skills.hasRuntimeSkills(h.orgId, h.danaId, true, "en")).toBe(false);
// Once the forced skill exists in the catalogue, everyone carries it.
importSkill(h, "humanizer", "humanizer");
expect(skills.hasRuntimeSkills(h.orgId, h.ceoId, true)).toBe(true);
expect(skills.hasRuntimeSkills(h.orgId, h.danaId, true)).toBe(true);
expect(skills.hasRuntimeSkills(h.orgId, h.ceoId, true, "en")).toBe(true);
expect(skills.hasRuntimeSkills(h.orgId, h.danaId, true, "en")).toBe(true);
// House style is not revocable, and granting it is a no-op with feedback.
const revoked = dispatchAgentTool(h.deps, h.ceoCtx, call("revoke_skill", { agent_name: "Dana", skill: "humanizer" }));
expect(revoked).toContain("ERROR");
expect(revoked).toContain("house style");
const granted = dispatchAgentTool(h.deps, h.ceoCtx, call("grant_skill", { agent_name: "Dana", skill: "humanizer" }));
expect(granted).toContain("automatically");
expect(skills.hasRuntimeSkills(h.orgId, h.danaId, true)).toBe(true);
expect(skills.hasRuntimeSkills(h.orgId, h.danaId, true, "en")).toBe(true);
});
});
+1 -1
View File
@@ -28,7 +28,7 @@ describe("editing a task", () => {
projectId: ids.projectA, goalId: ids.goalA, workspaceId: ids.wsA, priority: TASK_PRIORITY.NORMAL, requiresFilesystem: false,
});
});
const edit = (change: { projectId: string | null; goalId: string | null; workspaceId: string | null }) => connection.transaction(() => organization.updateTask({
const edit = (change: { projectId: string | null; goalId: string | null; workspaceId: string | null }) => connection.transaction(() => organization.updateTask({ workLanguage: null,
orgId: ids.org, taskId: ids.task, title: "Ship", description: "d", assigneeAgentId: null, risk: RISK_LEVEL.LOW, priority: TASK_PRIORITY.NORMAL,
parentTaskId: null, requiresFilesystem: true, writingMode: WRITING_MODE.TECHNICAL, ...change,
}));
+1 -1
View File
@@ -734,7 +734,7 @@ describe("task execution service", () => {
expect(connection.db.prepare("select count(*) as n from audit_events where entity_id = ? and action = 'stalled'").get(taskId)).toEqual({ n: 1 });
expect(notified).toBe(1);
organization.updateTask({ orgId, taskId, title: "Ship it", description: "Narrower scope.", assigneeAgentId: agentId, risk: RISK_LEVEL.LOW, priority: TASK_PRIORITY.NORMAL, parentTaskId: null, ...currentLinks(connection.db, taskId) });
organization.updateTask({ workLanguage: null, orgId, taskId, title: "Ship it", description: "Narrower scope.", assigneeAgentId: agentId, risk: RISK_LEVEL.LOW, priority: TASK_PRIORITY.NORMAL, parentTaskId: null, ...currentLinks(connection.db, taskId) });
expect(connection.db.prepare("select stalled_reason from tasks where id = ?").get(taskId)).toEqual({ stalled_reason: null });
expect(connection.transaction(() => execution.enqueueHeartbeatRuns())).toHaveLength(1);
+1 -1
View File
@@ -22,7 +22,7 @@ describe("task hierarchy", () => {
});
}
});
const reparent = (title: string, parent: string | null) => organization.updateTask({
const reparent = (title: string, parent: string | null) => organization.updateTask({ workLanguage: null,
orgId, taskId: ids[title], title, description: "d", assigneeAgentId: null, risk: RISK_LEVEL.LOW, priority: TASK_PRIORITY.NORMAL, parentTaskId: parent,
...currentLinks(connection.db, ids[title]),
});
+1 -1
View File
@@ -58,7 +58,7 @@ describe("task status authority", () => {
expect(h.as("dev", { status: "backlog", evidence: 123 })).toBe('ERROR: "evidence" is required.');
h.as("dev", { status: "done", evidence: "file_id=fil_1" });
const reviewId = (h.connection.db.prepare("select id from tasks where review_of_task_id = ?").get(h.ids.task) as { id: string }).id;
h.organization.updateTask({ orgId: h.orgId, taskId: h.ids.task, title: "Login page", description: "Build it.", assigneeAgentId: h.ids.cto, risk: RISK_LEVEL.LOW, priority: TASK_PRIORITY.NORMAL, parentTaskId: null, ...currentLinks(h.connection.db, h.ids.task) });
h.organization.updateTask({ workLanguage: null, orgId: h.orgId, taskId: h.ids.task, title: "Login page", description: "Build it.", assigneeAgentId: h.ids.cto, risk: RISK_LEVEL.LOW, priority: TASK_PRIORITY.NORMAL, parentTaskId: null, ...currentLinks(h.connection.db, h.ids.task) });
expect(h.status()).toBe("in_progress");
expect(h.connection.db.prepare("select status from tasks where id = ?").get(reviewId)).toEqual({ status: "done" });
expect(h.as("cto", { status: "done", evidence: "Rebuilt the form." })).toBe(`Task ${h.ids.task} moved to review: done needs sign-off from your manager. Cleo will review it.`);
+71
View File
@@ -0,0 +1,71 @@
import { randomUUID } from "node:crypto";
import { DatabaseSync } from "node:sqlite";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, test } from "vitest";
import { DatabaseConnection } from "../../src/main/database/connection";
import { OrganizationService } from "../../src/main/services/organization-service";
import { ChatService } from "../../src/main/services/chat-service";
import { dispatchAgentTool } from "../../src/main/services/agent-tools";
import { taskWorkLanguage } from "../../src/main/services/work-language";
import { workLanguageSchema } from "../../src/main/database/migrations/104-work-language";
import { RISK_LEVEL, TASK_PRIORITY, TASK_STATUS, WRITING_MODE } from "../../src/shared/domain";
describe("the working language", () => {
test("is the company's unless a task sets its own, and delegated work keeps its parent's", () => {
const connection = new DatabaseConnection(join(tmpdir(), `ac-work-language-${randomUUID()}.db`));
const db = connection.db;
const organization = new OrganizationService(db);
let orgId = "";
let ceoId = "";
let taskId = "";
connection.transaction(() => {
orgId = organization.createOrganization({ organizationName: "Acme", employeeName: "Yang", mission: "m", goal: "g" });
ceoId = organization.createAgent({ orgId, name: "CEO", role: "CEO", department: "Executive", modelProviderId: null, modelId: null, budgetCents: 1000, managerAgentId: null, roleLevel: "c_suite" });
taskId = organization.createTask({
orgId, title: "Plan", description: "d", status: TASK_STATUS.IN_PROGRESS, assigneeAgentId: ceoId, risk: RISK_LEVEL.LOW,
projectId: null, goalId: null, workspaceId: null, priority: TASK_PRIORITY.NORMAL, requiresFilesystem: false,
});
});
expect(taskWorkLanguage(db, orgId, taskId)).toBe("en");
// The display language plays no part.
organization.updateUiPreferences({ language: "zh" });
expect(taskWorkLanguage(db, orgId, taskId)).toBe("en");
organization.updateOrganization({
orgId, name: "Acme", description: "", mission: "m", goal: "g", executionMode: "ai_ceo", autoApproveHires: true, reviewDeliverables: false, workLanguage: "zh",
});
expect(taskWorkLanguage(db, orgId, taskId)).toBe("zh");
const edit = (workLanguage: "en" | "zh" | null) => connection.transaction(() => organization.updateTask({
orgId, taskId, title: "Plan", description: "d", assigneeAgentId: ceoId, risk: RISK_LEVEL.LOW, priority: TASK_PRIORITY.NORMAL,
parentTaskId: null, projectId: null, goalId: null, workspaceId: null, requiresFilesystem: false, writingMode: WRITING_MODE.CONVERSATIONAL, workLanguage,
}));
edit("en");
expect(taskWorkLanguage(db, orgId, taskId)).toBe("en");
const delegated = dispatchAgentTool({ db, organization, chat: new ChatService(db) }, {
orgId, agent: { id: ceoId, modelProviderId: null, modelId: null }, taskProjectId: null, taskGoalId: null, taskId, sourceThreadId: null, workspace: null,
}, { id: "call_1", name: "create_task", arguments: { title: "Draft", description: "d" } });
const childId = /task_id=([^)\s]+)\)/.exec(delegated)![1];
expect(db.prepare("select work_language from tasks where id = ?").get(childId)).toEqual({ work_language: "en" });
edit(null);
expect(taskWorkLanguage(db, orgId, taskId)).toBe("zh");
});
test("an existing company keeps the language its agents had, taken from the app setting", () => {
const db = new DatabaseSync(":memory:");
db.exec(`
create table schema_migrations(version integer, name text, applied_at text);
create table app_state(id integer primary key, language text);
create table organizations(id text primary key);
create table tasks(id text primary key, org_id text);
insert into app_state(id, language) values (1, 'zh');
insert into organizations(id) values ('org_a');
`);
db.exec(workLanguageSchema);
expect(db.prepare("select work_language from organizations").get()).toEqual({ work_language: "zh" });
});
});