Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,16 @@ const HERMES_ROLES: Role[] = [
descriptionKey: "hermesRoleDelegationDesc",
},
{ id: "vision", labelKey: "hermesRoleVision", descriptionKey: "hermesRoleVisionDesc" },
{
id: "compression",
labelKey: "hermesRoleCompression",
descriptionKey: "hermesRoleCompressionDesc",
},
{
id: "web_extract",
labelKey: "hermesRoleWebExtract",
descriptionKey: "hermesRoleWebExtractDesc",
},
{
id: "compression",
labelKey: "hermesRoleCompression",
descriptionKey: "hermesRoleCompressionDesc",
},
{
id: "skills_hub",
labelKey: "hermesRoleSkillsHub",
Expand All @@ -38,6 +38,45 @@ const HERMES_ROLES: Role[] = [
labelKey: "hermesRoleApproval",
descriptionKey: "hermesRoleApprovalDesc",
},
{ id: "mcp", labelKey: "hermesRoleMcp", descriptionKey: "hermesRoleMcpDesc" },
{
id: "title_generation",
labelKey: "hermesRoleTitleGeneration",
descriptionKey: "hermesRoleTitleGenerationDesc",
},
{
id: "memory_query_rewrite",
labelKey: "hermesRoleMemoryQueryRewrite",
descriptionKey: "hermesRoleMemoryQueryRewriteDesc",
},
{
id: "tts_audio_tags",
labelKey: "hermesRoleTtsAudioTags",
descriptionKey: "hermesRoleTtsAudioTagsDesc",
},
{
id: "triage_specifier",
labelKey: "hermesRoleTriageSpecifier",
descriptionKey: "hermesRoleTriageSpecifierDesc",
},
{
id: "kanban_decomposer",
labelKey: "hermesRoleKanbanDecomposer",
descriptionKey: "hermesRoleKanbanDecomposerDesc",
},
{
id: "profile_describer",
labelKey: "hermesRoleProfileDescriber",
descriptionKey: "hermesRoleProfileDescriberDesc",
},
{ id: "goal_judge", labelKey: "hermesRoleGoalJudge", descriptionKey: "hermesRoleGoalJudgeDesc" },
{ id: "curator", labelKey: "hermesRoleCurator", descriptionKey: "hermesRoleCuratorDesc" },
{ id: "monitor", labelKey: "hermesRoleMonitor", descriptionKey: "hermesRoleMonitorDesc" },
{
id: "background_review",
labelKey: "hermesRoleBackgroundReview",
descriptionKey: "hermesRoleBackgroundReviewDesc",
},
];

const HERMES_AGENT_ZERO_CONFIG_PROVIDERS = ["opencode"];
Expand Down
22 changes: 22 additions & 0 deletions src/i18n/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2905,6 +2905,28 @@
"hermesRoleSkillsHubDesc": "Skills and tool-use reasoning",
"hermesRoleApproval": "Approval",
"hermesRoleApprovalDesc": "Safety and approval decisions",
"hermesRoleMcp": "MCP",
"hermesRoleMcpDesc": "MCP server tool calls",
"hermesRoleTitleGeneration": "Title Generation",
"hermesRoleTitleGenerationDesc": "Session title generation",
"hermesRoleMemoryQueryRewrite": "Memory Query Rewrite",
"hermesRoleMemoryQueryRewriteDesc": "Memory search query rewriting",
"hermesRoleTtsAudioTags": "TTS Audio Tags",
"hermesRoleTtsAudioTagsDesc": "TTS audio tag generation",
"hermesRoleTriageSpecifier": "Triage Specifier",
"hermesRoleTriageSpecifierDesc": "Issue and PR triage specification",
"hermesRoleKanbanDecomposer": "Kanban Decomposer",
"hermesRoleKanbanDecomposerDesc": "Kanban task decomposition",
"hermesRoleProfileDescriber": "Profile Describer",
"hermesRoleProfileDescriberDesc": "User profile description",
"hermesRoleGoalJudge": "Goal Judge",
"hermesRoleGoalJudgeDesc": "Goal completion judging",
"hermesRoleCurator": "Curator",
"hermesRoleCuratorDesc": "Skill and memory curation",
"hermesRoleMonitor": "Monitor",
"hermesRoleMonitorDesc": "Background monitoring",
"hermesRoleBackgroundReview": "Background Review",
"hermesRoleBackgroundReviewDesc": "Background code review",
"hermesSelectBeforePreview": "Select models for roles, or ensure the roles are loaded, before previewing.",
"hermesPreviewFailed": "Failed to generate preview",
"hermesSavedTo": "Saved to {path}",
Expand Down
22 changes: 22 additions & 0 deletions src/i18n/messages/pl.json
Original file line number Diff line number Diff line change
Expand Up @@ -2905,6 +2905,28 @@
"hermesRoleSkillsHubDesc": "Wnioskowanie o umiejętnościach i użyciu narzędzi",
"hermesRoleApproval": "Zatwierdzanie",
"hermesRoleApprovalDesc": "Decyzje dotyczące bezpieczeństwa i zatwierdzania",
"hermesRoleMcp": "MCP",
"hermesRoleMcpDesc": "Wywołania narzędzi serwera MCP",
"hermesRoleTitleGeneration": "Generowanie tytułów",
"hermesRoleTitleGenerationDesc": "Generowanie tytułów sesji",
"hermesRoleMemoryQueryRewrite": "Przepisywanie zapytań pamięci",
"hermesRoleMemoryQueryRewriteDesc": "Przepisywanie zapytań wyszukiwania pamięci",
"hermesRoleTtsAudioTags": "Tagi audio TTS",
"hermesRoleTtsAudioTagsDesc": "Generowanie tagów audio TTS",
"hermesRoleTriageSpecifier": "Specyfikacja triage",
"hermesRoleTriageSpecifierDesc": "Specyfikacja triage issue i PR",
"hermesRoleKanbanDecomposer": "Dekompozycja kanban",
"hermesRoleKanbanDecomposerDesc": "Dekompozycja zadań kanban",
"hermesRoleProfileDescriber": "Opis profilu",
"hermesRoleProfileDescriberDesc": "Opis profilu użytkownika",
"hermesRoleGoalJudge": "Ocena celów",
"hermesRoleGoalJudgeDesc": "Ocena realizacji celów",
"hermesRoleCurator": "Kurator",
"hermesRoleCuratorDesc": "Kuracja umiejętności i pamięci",
"hermesRoleMonitor": "Monitor",
"hermesRoleMonitorDesc": "Monitorowanie w tle",
"hermesRoleBackgroundReview": "Przegląd w tle",
"hermesRoleBackgroundReviewDesc": "Przegląd kodu w tle",
"hermesSelectBeforePreview": "Wybierz modele dla ról lub upewnij się, że role są załadowane przed wyświetleniem podglądu.",
"hermesPreviewFailed": "Nie udało się wygenerować podglądu",
"hermesSavedTo": "Zapisano w {path}",
Expand Down
22 changes: 22 additions & 0 deletions src/i18n/messages/pt-BR.json
Original file line number Diff line number Diff line change
Expand Up @@ -2905,6 +2905,28 @@
"hermesRoleSkillsHubDesc": "Raciocínio de habilidades e uso de ferramentas",
"hermesRoleApproval": "Aprovação",
"hermesRoleApprovalDesc": "Decisões de segurança e aprovação",
"hermesRoleMcp": "MCP",
"hermesRoleMcpDesc": "Chamadas de ferramentas de servidores MCP",
"hermesRoleTitleGeneration": "Geração de Título",
"hermesRoleTitleGenerationDesc": "Geração do título da sessão",
"hermesRoleMemoryQueryRewrite": "Reescrita de Consulta de Memória",
"hermesRoleMemoryQueryRewriteDesc": "Reescrita das consultas de busca na memória",
"hermesRoleTtsAudioTags": "Tags de Áudio TTS",
"hermesRoleTtsAudioTagsDesc": "Geração de tags de áudio para TTS",
"hermesRoleTriageSpecifier": "Especificador de Triagem",
"hermesRoleTriageSpecifierDesc": "Especificação de triagem de issues e PRs",
"hermesRoleKanbanDecomposer": "Decompositor de Kanban",
"hermesRoleKanbanDecomposerDesc": "Decomposição de tarefas no Kanban",
"hermesRoleProfileDescriber": "Descritor de Perfil",
"hermesRoleProfileDescriberDesc": "Descrição do perfil do usuário",
"hermesRoleGoalJudge": "Juiz de Objetivos",
"hermesRoleGoalJudgeDesc": "Julgamento da conclusão de objetivos",
"hermesRoleCurator": "Curador",
"hermesRoleCuratorDesc": "Curadoria de habilidades e memória",
"hermesRoleMonitor": "Monitor",
"hermesRoleMonitorDesc": "Monitoramento em segundo plano",
"hermesRoleBackgroundReview": "Revisão em Segundo Plano",
"hermesRoleBackgroundReviewDesc": "Revisão de código em segundo plano",
"hermesSelectBeforePreview": "Selecione os modelos para as funções, ou certifique-se de que as funções estejam carregadas, antes de visualizar.",
"hermesPreviewFailed": "Falha ao gerar visualização",
"hermesSavedTo": "Salvo em {path}",
Expand Down
22 changes: 22 additions & 0 deletions src/i18n/messages/vi.json
Original file line number Diff line number Diff line change
Expand Up @@ -2905,6 +2905,28 @@
"hermesRoleSkillsHubDesc": "Suy luận về kỹ năng và cách dùng công cụ",
"hermesRoleApproval": "Phê duyệt",
"hermesRoleApprovalDesc": "Quyết định về an toàn và phê duyệt",
"hermesRoleMcp": "MCP",
"hermesRoleMcpDesc": "Lệnh gọi công cụ của máy chủ MCP",
"hermesRoleTitleGeneration": "Tạo tiêu đề",
"hermesRoleTitleGenerationDesc": "Tạo tiêu đề cho phiên làm việc",
"hermesRoleMemoryQueryRewrite": "Viết lại truy vấn bộ nhớ",
"hermesRoleMemoryQueryRewriteDesc": "Viết lại truy vấn tìm kiếm trong bộ nhớ",
"hermesRoleTtsAudioTags": "Thẻ âm thanh TTS",
"hermesRoleTtsAudioTagsDesc": "Tạo thẻ âm thanh cho TTS",
"hermesRoleTriageSpecifier": "Bộ đặc tả phân loại",
"hermesRoleTriageSpecifierDesc": "Đặc tả phân loại issue và PR",
"hermesRoleKanbanDecomposer": "Bộ phân rã Kanban",
"hermesRoleKanbanDecomposerDesc": "Phân rã công việc trên Kanban",
"hermesRoleProfileDescriber": "Bộ mô tả hồ sơ",
"hermesRoleProfileDescriberDesc": "Mô tả hồ sơ người dùng",
"hermesRoleGoalJudge": "Bộ đánh giá mục tiêu",
"hermesRoleGoalJudgeDesc": "Đánh giá mức độ hoàn thành mục tiêu",
"hermesRoleCurator": "Bộ tuyển chọn",
"hermesRoleCuratorDesc": "Tuyển chọn kỹ năng và bộ nhớ",
"hermesRoleMonitor": "Giám sát",
"hermesRoleMonitorDesc": "Giám sát chạy nền",
"hermesRoleBackgroundReview": "Đánh giá chạy nền",
"hermesRoleBackgroundReviewDesc": "Đánh giá mã nguồn chạy nền",
"hermesSelectBeforePreview": "Hãy chọn mô hình cho các vai trò hoặc bảo đảm vai trò đã được tải trước khi xem trước.",
"hermesPreviewFailed": "Không thể tạo bản xem trước",
"hermesSavedTo": "Đã lưu vào {path}",
Expand Down
29 changes: 28 additions & 1 deletion src/lib/cli-helper/config-generator/hermes-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,37 @@ export const HERMES_AGENT_ROLES = [
description: "Orchestrator and sub-agent spawning model",
},
{ id: "vision", label: "Vision", description: "Image and screenshot understanding" },
{ id: "compression", label: "Compression", description: "Prompt compression and summarization" },
{ id: "web_extract", label: "Web Extract", description: "Web page / content extraction" },
{ id: "compression", label: "Compression", description: "Prompt compression and summarization" },
{ id: "skills_hub", label: "Skills Hub", description: "Skills and tool-use reasoning" },
{ id: "approval", label: "Approval", description: "Safety and approval decisions" },
{ id: "mcp", label: "MCP", description: "MCP server tool calls" },
{ id: "title_generation", label: "Title Generation", description: "Session title generation" },
{
id: "memory_query_rewrite",
label: "Memory Query Rewrite",
description: "Memory search query rewriting",
},
{ id: "tts_audio_tags", label: "TTS Audio Tags", description: "TTS audio tag generation" },
{
id: "triage_specifier",
label: "Triage Specifier",
description: "Issue / PR triage specification",
},
{
id: "kanban_decomposer",
label: "Kanban Decomposer",
description: "Kanban task decomposition",
},
{ id: "profile_describer", label: "Profile Describer", description: "User profile description" },
{ id: "goal_judge", label: "Goal Judge", description: "Goal completion judging" },
{ id: "curator", label: "Curator", description: "Skill and memory curation" },
{ id: "monitor", label: "Monitor", description: "Background monitoring" },
{
id: "background_review",
label: "Background Review",
description: "Background code review",
},
] as const;

export type HermesAgentRole = (typeof HERMES_AGENT_ROLES)[number]["id"];
Expand Down
114 changes: 114 additions & 0 deletions tests/unit/cli-helper/config-generator.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,34 @@
import { describe, it } from "node:test";
import assert from "node:assert";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import * as generator from "../../../src/lib/cli-helper/config-generator/index.ts";

// The UI's HERMES_ROLES catalog (HermesAgentToolCard.tsx) is a "use client" component
// module — importing it in the Node test runner would pull in React/JSX. Instead we
// extract the id list straight from source text, which is enough to diff catalogs
// without executing the component.
function readUiHermesRoleIds(): string[] {
const uiFilePath = fileURLToPath(
new URL(
"../../../src/app/(dashboard)/dashboard/cli-code/components/HermesAgentToolCard.tsx",
import.meta.url
)
);
const source = readFileSync(uiFilePath, "utf-8");
const arrayMatch = source.match(/const HERMES_ROLES: Role\[\] = \[([\s\S]*?)\n\];/);
assert.ok(arrayMatch, "could not locate HERMES_ROLES array in HermesAgentToolCard.tsx");
const body = arrayMatch[1];
return Array.from(body.matchAll(/id:\s*"([a-z0-9_]+)"/g)).map((m) => m[1]);
}

function readEnMessages(): { cliTools?: Record<string, string> } {
const enJsonPath = fileURLToPath(
new URL("../../../src/i18n/messages/en.json", import.meta.url)
);
return JSON.parse(readFileSync(enJsonPath, "utf-8"));
}

describe("config-generator", () => {
describe("validateBaseUrl", () => {
it("accepts http URLs", async () => {
Expand Down Expand Up @@ -133,6 +160,93 @@ describe("config-generator", () => {
assert.ok(ids.includes("delegation"));
assert.ok(ids.includes("vision"));
assert.ok(ids.includes("approval"));
// Full catalog, including the 11 auxiliary roles added alongside HERMES_AGENT_ROLES
// (mcp, title_generation, memory_query_rewrite, tts_audio_tags, triage_specifier,
// kanban_decomposer, profile_describer, goal_judge, curator, monitor,
// background_review). Listed explicitly (not just parity-diffed against the UI
// below) so a role dropped from BOTH catalogs at once still fails this test.
const expectedIds = [
"default",
"delegation",
"vision",
"web_extract",
"compression",
"skills_hub",
"approval",
"mcp",
"title_generation",
"memory_query_rewrite",
"tts_audio_tags",
"triage_specifier",
"kanban_decomposer",
"profile_describer",
"goal_judge",
"curator",
"monitor",
"background_review",
];
assert.deepStrictEqual([...ids].sort(), [...expectedIds].sort());
});

it("keeps the backend HERMES_AGENT_ROLES catalog in sync with the UI's HERMES_ROLES catalog", async () => {
// The UI (HermesAgentToolCard.tsx) maintains its own parallel role catalog for
// rendering the role dropdowns. Nothing at the type level keeps the two catalogs
// in sync, so a role added to one and not the other would ship silently (the
// backend would accept a role the UI never offers, or the UI would offer a role
// the backend config generator doesn't know how to place in the YAML). Diffing
// the id lists turns that drift into a CI failure instead.
const hermesAgent =
await import("../../../src/lib/cli-helper/config-generator/hermes-agent.ts");
const backendIds: string[] = hermesAgent.HERMES_AGENT_ROLES.map((r) => r.id);
const uiIds = readUiHermesRoleIds();

const missingFromUi = backendIds.filter((id) => !uiIds.includes(id));
const missingFromBackend = uiIds.filter((id) => !backendIds.includes(id));

assert.deepStrictEqual(
missingFromUi,
[],
`role ids present in backend HERMES_AGENT_ROLES but missing from UI HERMES_ROLES: ${missingFromUi.join(", ")}`
);
assert.deepStrictEqual(
missingFromBackend,
[],
`role ids present in UI HERMES_ROLES but missing from backend HERMES_AGENT_ROLES: ${missingFromBackend.join(", ")}`
);
});

it("resolves labelKey/descriptionKey for every HERMES_AGENT_ROLES id in en.json's cliTools namespace", async () => {
// Generalizes the exact bug class the contributor had to hand-fix in their 2nd
// commit (missing vi/pt-BR translations for the new roles): every role's
// labelKey/descriptionKey must exist as a real key under cliTools in en.json,
// the source-of-truth locale, or the UI silently renders the raw key string.
const uiFilePath = fileURLToPath(
new URL(
"../../../src/app/(dashboard)/dashboard/cli-code/components/HermesAgentToolCard.tsx",
import.meta.url
)
);
const source = readFileSync(uiFilePath, "utf-8");
const arrayMatch = source.match(/const HERMES_ROLES: Role\[\] = \[([\s\S]*?)\n\];/);
assert.ok(arrayMatch, "could not locate HERMES_ROLES array in HermesAgentToolCard.tsx");
const body = arrayMatch[1];
const roleEntries = Array.from(
body.matchAll(/id:\s*"([a-z0-9_]+)"[\s\S]*?labelKey:\s*"([A-Za-z0-9]+)"[\s\S]*?descriptionKey:\s*"([A-Za-z0-9]+)"/g)
).map((m) => ({ id: m[1], labelKey: m[2], descriptionKey: m[3] }));
assert.ok(roleEntries.length > 0, "expected at least one role entry to be parsed");

const en = readEnMessages();
const cliTools = en.cliTools || {};
const missing: string[] = [];
for (const { id, labelKey, descriptionKey } of roleEntries) {
if (typeof cliTools[labelKey] !== "string") {
missing.push(`${id}: labelKey "${labelKey}"`);
}
if (typeof cliTools[descriptionKey] !== "string") {
missing.push(`${id}: descriptionKey "${descriptionKey}"`);
}
}
assert.deepStrictEqual(missing, [], `missing en.json cliTools keys: ${missing.join("; ")}`);
});

it("getCurrentHermesAgentRoles returns an object", async () => {
Expand Down
Loading