From 63503ebb14069e8ba0bea91955e7ce4e01670a4e Mon Sep 17 00:00:00 2001 From: Austin Pickett Date: Fri, 15 May 2026 22:40:21 -0400 Subject: [PATCH 1/4] fix(dashboard): clarify Kanban Ready vs assignment Ready column help and fallbacks now describe dependency-ready work; show a badge on unassigned ready cards and fix the stale unassigned tooltip. Align localized Ready help strings with the new semantics. Co-authored-by: Cursor --- plugins/kanban/dashboard/dist/index.js | 14 ++++++++++++-- plugins/kanban/dashboard/dist/style.css | 8 ++++++++ web/src/i18n/af.ts | 2 +- web/src/i18n/de.ts | 2 +- web/src/i18n/en.ts | 5 ++++- web/src/i18n/es.ts | 2 +- web/src/i18n/fr.ts | 2 +- web/src/i18n/ga.ts | 2 +- web/src/i18n/hu.ts | 2 +- web/src/i18n/it.ts | 2 +- web/src/i18n/ja.ts | 2 +- web/src/i18n/ko.ts | 2 +- web/src/i18n/pt.ts | 2 +- web/src/i18n/ru.ts | 2 +- web/src/i18n/tr.ts | 2 +- web/src/i18n/types.ts | 2 ++ web/src/i18n/zh-hant.ts | 2 +- web/src/i18n/zh.ts | 2 +- 18 files changed, 40 insertions(+), 17 deletions(-) diff --git a/plugins/kanban/dashboard/dist/index.js b/plugins/kanban/dashboard/dist/index.js index 720cdb9e1e2..6f05df72bf6 100644 --- a/plugins/kanban/dashboard/dist/index.js +++ b/plugins/kanban/dashboard/dist/index.js @@ -68,7 +68,7 @@ const FALLBACK_COLUMN_HELP = { triage: "Raw ideas — a specifier will flesh out the spec", todo: "Waiting on dependencies or unassigned", - ready: "Assigned and waiting for a dispatcher tick", + ready: "Dependencies satisfied; assign a profile to dispatch", running: "Claimed by a worker — in-flight", blocked: "Worker asked for human input", done: "Completed", @@ -2048,6 +2048,7 @@ }; const progress = t.progress; + const needsAssignee = t.status === "ready" && !t.assignee; return h("div", { ref: cardRef, @@ -2118,6 +2119,13 @@ title: `${progress.done} of ${progress.total} child tasks done`, }, `${progress.done}/${progress.total}`) : null, + needsAssignee + ? h(Badge, { + variant: "outline", + className: "hermes-kanban-needs-assignee", + title: tx(i18n, "needsAssigneeHint", "Dependencies are satisfied, but the dispatcher skips this task until you assign a profile."), + }, tx(i18n, "needsAssignee", "Needs assignee")) + : null, ), h("div", { className: "hermes-kanban-card-title" }, t.title || tx(i18n, "untitled", "(untitled)")), @@ -2126,7 +2134,9 @@ ? h("span", { className: "hermes-kanban-assignee", title: `Assigned to Hermes profile @${t.assignee}` }, "@", t.assignee) : h("span", { className: "hermes-kanban-unassigned", - title: "No profile assigned. The dispatcher will pick one from available profiles when the task is Ready." }, + title: needsAssignee + ? tx(i18n, "needsAssigneeHint", "Dependencies are satisfied, but the dispatcher skips this task until you assign a profile.") + : "No profile assigned." }, tx(i18n, "unassigned", "unassigned")), t.comment_count > 0 ? h("span", { className: "hermes-kanban-count", diff --git a/plugins/kanban/dashboard/dist/style.css b/plugins/kanban/dashboard/dist/style.css index 3bcfccb289b..f3d66a88597 100644 --- a/plugins/kanban/dashboard/dist/style.css +++ b/plugins/kanban/dashboard/dist/style.css @@ -280,6 +280,14 @@ padding: 0.05rem 0.3rem !important; } +.hermes-kanban-needs-assignee { + font-size: 0.6rem !important; + padding: 0.05rem 0.3rem !important; + background: color-mix(in srgb, var(--color-warning, #d4b348) 16%, transparent); + border-color: color-mix(in srgb, var(--color-warning, #d4b348) 45%, var(--color-border)); + color: var(--color-foreground); +} + .hermes-kanban-assignee { font-weight: 500; color: color-mix(in srgb, var(--color-foreground) 80%, var(--color-muted-foreground)); diff --git a/web/src/i18n/af.ts b/web/src/i18n/af.ts index 4f49eb12227..e588a63596d 100644 --- a/web/src/i18n/af.ts +++ b/web/src/i18n/af.ts @@ -663,7 +663,7 @@ export const af: Translations = { columnHelp: { triage: "Rou idees — 'n spesifiseerder sal die spesifikasie uitwerk", todo: "Wag op afhanklikhede of nie toegewys nie", - ready: "Toegewys en wag vir 'n versender-tik", + ready: "Afhanklikhede is bevredig; wys 'n profiel toe om te versend", running: "Deur 'n werker geëis — in vlug", blocked: "Werker het mensinvoer aangevra", done: "Voltooi", diff --git a/web/src/i18n/de.ts b/web/src/i18n/de.ts index c70ccfe8701..28a9b59deff 100644 --- a/web/src/i18n/de.ts +++ b/web/src/i18n/de.ts @@ -662,7 +662,7 @@ export const de: Translations = { columnHelp: { triage: "Rohe Ideen — ein Specifier wird die Spezifikation ausarbeiten", todo: "Wartet auf Abhängigkeiten oder ist nicht zugewiesen", - ready: "Zugewiesen und wartet auf einen Dispatcher-Tick", + ready: "Abhängigkeiten erfüllt; Profil zum Dispatch zuweisen", running: "Von einem Worker übernommen — in Bearbeitung", blocked: "Worker hat um menschliche Eingabe gebeten", done: "Abgeschlossen", diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index e93fdac7ec4..5eae3f9a14a 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -574,6 +574,9 @@ export const en: Translations = { createTask: "Create task in this column", noTasks: "— no tasks —", unassigned: "unassigned", + needsAssignee: "Needs assignee", + needsAssigneeHint: + "Dependencies are satisfied, but the dispatcher skips this task until you assign a profile.", untitled: "(untitled)", loadingDetail: "Loading…", addComment: "Add a comment… (Enter to submit)", @@ -664,7 +667,7 @@ export const en: Translations = { columnHelp: { triage: "Raw ideas — a specifier will flesh out the spec", todo: "Waiting on dependencies or unassigned", - ready: "Assigned and waiting for a dispatcher tick", + ready: "Dependencies satisfied; assign a profile to dispatch", running: "Claimed by a worker — in-flight", blocked: "Worker asked for human input", done: "Completed", diff --git a/web/src/i18n/es.ts b/web/src/i18n/es.ts index 19088de12c8..139a8175d44 100644 --- a/web/src/i18n/es.ts +++ b/web/src/i18n/es.ts @@ -662,7 +662,7 @@ export const es: Translations = { columnHelp: { triage: "Ideas en bruto — un specifier desarrollará la especificación", todo: "Esperando dependencias o sin asignar", - ready: "Asignado y esperando un tick del dispatcher", + ready: "Dependencias satisfechas; asigna un perfil para despachar", running: "Reclamado por un worker — en ejecución", blocked: "El worker pidió intervención humana", done: "Completado", diff --git a/web/src/i18n/fr.ts b/web/src/i18n/fr.ts index 4532cab3ee0..51b5ba54f12 100644 --- a/web/src/i18n/fr.ts +++ b/web/src/i18n/fr.ts @@ -662,7 +662,7 @@ export const fr: Translations = { columnHelp: { triage: "Idées brutes — un specifier rédigera la spécification", todo: "En attente de dépendances ou non assigné", - ready: "Assigné et en attente d'un tick du dispatcher", + ready: "Dépendances satisfaites ; assignez un profil pour dispatch", running: "Réclamé par un worker — en cours d'exécution", blocked: "Le worker a demandé une intervention humaine", done: "Terminé", diff --git a/web/src/i18n/ga.ts b/web/src/i18n/ga.ts index d75ec061b8b..4dc4e823430 100644 --- a/web/src/i18n/ga.ts +++ b/web/src/i18n/ga.ts @@ -663,7 +663,7 @@ export const ga: Translations = { columnHelp: { triage: "Smaointe amha — déanfaidh specifier an spec a chur i bhfeidhm", todo: "Ag fanacht ar spleáchais nó gan sannadh", - ready: "Sannta agus ag fanacht ar thic an dispatcher", + ready: "Tá na spleáchais sásaithe; sann próifíl le dispatch a dhéanamh", running: "Éilithe ag worker — ar siúl", blocked: "D'iarr an worker ionchur duine", done: "Críochnaithe", diff --git a/web/src/i18n/hu.ts b/web/src/i18n/hu.ts index f563c1dacc4..8b492f3bb16 100644 --- a/web/src/i18n/hu.ts +++ b/web/src/i18n/hu.ts @@ -663,7 +663,7 @@ export const hu: Translations = { columnHelp: { triage: "Nyers ötletek — egy specifier kidolgozza a specifikációt", todo: "Függőségekre vár vagy nincs felelőse", - ready: "Kiosztva, dispatcher tickre vár", + ready: "A függőségek teljesültek; rendelj hozzá profilt az indításhoz", running: "Worker felvette — folyamatban", blocked: "A worker emberi beavatkozást kért", done: "Befejezve", diff --git a/web/src/i18n/it.ts b/web/src/i18n/it.ts index 5e79d3115c3..86fce86589e 100644 --- a/web/src/i18n/it.ts +++ b/web/src/i18n/it.ts @@ -662,7 +662,7 @@ export const it: Translations = { columnHelp: { triage: "Idee grezze — un specifier elaborerà la specifica", todo: "In attesa di dipendenze o non assegnato", - ready: "Assegnato e in attesa di un tick del dispatcher", + ready: "Dipendenze soddisfatte; assegna un profilo per il dispatch", running: "Preso in carico da un worker — in esecuzione", blocked: "Il worker ha richiesto input umano", done: "Completato", diff --git a/web/src/i18n/ja.ts b/web/src/i18n/ja.ts index 175468e4d8b..154e11f5dbb 100644 --- a/web/src/i18n/ja.ts +++ b/web/src/i18n/ja.ts @@ -663,7 +663,7 @@ export const ja: Translations = { columnHelp: { triage: "未整理のアイデア — スペシファイアが仕様を肉付けします", todo: "依存関係の待機中、または未割り当て", - ready: "割り当て済み、ディスパッチャーのティック待ち", + ready: "依存関係は満たされています。ディスパッチするにはプロファイルを割り当ててください", running: "ワーカーが取得中 — 実行中", blocked: "ワーカーが人間の入力を求めています", done: "完了", diff --git a/web/src/i18n/ko.ts b/web/src/i18n/ko.ts index cfc40d63df7..4dafaeb9cde 100644 --- a/web/src/i18n/ko.ts +++ b/web/src/i18n/ko.ts @@ -663,7 +663,7 @@ export const ko: Translations = { columnHelp: { triage: "원시 아이디어 — 스페시파이어가 사양을 구체화합니다", todo: "종속성 대기 중 또는 미지정", - ready: "지정되었으며 디스패처 틱 대기 중", + ready: "종속성이 충족됨; 디스패치하려면 프로필을 지정하세요", running: "워커가 점유 중 — 실행 중", blocked: "워커가 사람의 입력을 요청함", done: "완료됨", diff --git a/web/src/i18n/pt.ts b/web/src/i18n/pt.ts index 6cdd40b8fe5..d32402dc92a 100644 --- a/web/src/i18n/pt.ts +++ b/web/src/i18n/pt.ts @@ -663,7 +663,7 @@ export const pt: Translations = { columnHelp: { triage: "Ideias em bruto — um specifier vai detalhar a especificação", todo: "À espera de dependências ou sem atribuição", - ready: "Atribuído e à espera de um tick do dispatcher", + ready: "Dependências satisfeitas; atribua um perfil para despachar", running: "Reivindicado por um worker — em execução", blocked: "O worker pediu intervenção humana", done: "Concluído", diff --git a/web/src/i18n/ru.ts b/web/src/i18n/ru.ts index c5b9a5b5038..79a6961b251 100644 --- a/web/src/i18n/ru.ts +++ b/web/src/i18n/ru.ts @@ -663,7 +663,7 @@ export const ru: Translations = { columnHelp: { triage: "Сырые идеи — specifier подготовит спецификацию", todo: "Ожидает зависимостей или без исполнителя", - ready: "Назначено и ждёт тика диспетчера", + ready: "Зависимости выполнены; назначьте профиль для диспетчеризации", running: "Взято воркером — выполняется", blocked: "Воркер запросил вмешательство человека", done: "Завершено", diff --git a/web/src/i18n/tr.ts b/web/src/i18n/tr.ts index 7de6ea1df7d..56670424abb 100644 --- a/web/src/i18n/tr.ts +++ b/web/src/i18n/tr.ts @@ -663,7 +663,7 @@ export const tr: Translations = { columnHelp: { triage: "Ham fikirler — bir specifier şartnameyi detaylandıracak", todo: "Bağımlılıklar bekleniyor veya atanmamış", - ready: "Atanmış ve dispatcher tick'i bekleniyor", + ready: "Bağımlılıklar karşılandı; dispatch için bir profil atayın", running: "Bir worker tarafından alındı — yürütülüyor", blocked: "Worker insan girdisi istedi", done: "Tamamlandı", diff --git a/web/src/i18n/types.ts b/web/src/i18n/types.ts index ca40b4a381f..55669a4b679 100644 --- a/web/src/i18n/types.ts +++ b/web/src/i18n/types.ts @@ -586,6 +586,8 @@ export interface Translations { createTask: string; noTasks: string; unassigned: string; + needsAssignee?: string; + needsAssigneeHint?: string; untitled: string; loadingDetail: string; addComment: string; diff --git a/web/src/i18n/zh-hant.ts b/web/src/i18n/zh-hant.ts index c79222cfe91..27f3a41b95f 100644 --- a/web/src/i18n/zh-hant.ts +++ b/web/src/i18n/zh-hant.ts @@ -663,7 +663,7 @@ export const zhHant: Translations = { columnHelp: { triage: "原始想法 — 規格制定者將完善規格", todo: "等待相依項目或尚未指派", - ready: "已指派,等待排程器輪詢", + ready: "相依項目已滿足;指派設定檔以便排程", running: "已被工作者領取 — 執行中", blocked: "工作者請求人工輸入", done: "已完成", diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index 0a8ceb7962a..6290c473b82 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -659,7 +659,7 @@ export const zh: Translations = { columnHelp: { triage: "原始想法 — 规范制定者将完善规格", todo: "等待依赖项或未分配", - ready: "已分配,等待调度器轮询", + ready: "依赖项已满足;分配一个配置文件以便调度", running: "已被工作者认领 — 执行中", blocked: "工作者请求人工输入", done: "已完成", From ca413c6164e7957d33841353feb9cdbf838dead7 Mon Sep 17 00:00:00 2001 From: Austin Pickett Date: Fri, 15 May 2026 23:39:29 -0400 Subject: [PATCH 2/4] fix(dashboard): align Ukrainian Kanban Ready column help Mirrors the dependency-ready / assign-profile semantics used in other locales; Copilot review noted uk.ts was still on the old dispatcher-tick wording. Co-authored-by: Cursor --- web/src/i18n/uk.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/i18n/uk.ts b/web/src/i18n/uk.ts index 72726aabe5f..3c3df8dae68 100644 --- a/web/src/i18n/uk.ts +++ b/web/src/i18n/uk.ts @@ -663,7 +663,7 @@ export const uk: Translations = { columnHelp: { triage: "Сирі ідеї — специфікатор деталізує специфікацію", todo: "Очікує на залежності або не призначено", - ready: "Призначено, очікує тіку диспетчера", + ready: "Залежності задоволені; призначте профіль для диспетчеризації", running: "Захоплено воркером — у роботі", blocked: "Воркер запитав втручання людини", done: "Завершено", From 965610f922be5b2afb6fa412205077486734a433 Mon Sep 17 00:00:00 2001 From: Austin Pickett Date: Fri, 15 May 2026 23:57:30 -0400 Subject: [PATCH 3/4] fix(cli): tolerate unreadable dirs when building systemd PATH MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit generate_systemd_unit runs _build_service_path_dirs(); tests that mimic sudo (Path.home → /root) caused is_dir() to raise PermissionError for unprivileged users on /root/.hermes/..., failing CI. Treat inaccessible paths like missing. Co-authored-by: Cursor --- hermes_cli/gateway.py | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/hermes_cli/gateway.py b/hermes_cli/gateway.py index a865bcaf8be..f2d6223f3de 100644 --- a/hermes_cli/gateway.py +++ b/hermes_cli/gateway.py @@ -2103,6 +2103,19 @@ def _hermes_home_for_target_user(target_home_dir: str) -> str: return str(current_hermes) +def _path_usable_bindir(path: Path) -> bool: + """True iff ``path`` exists as a dir and we could stat/read it. + + systemd unit generation may run under simulated ``sudo`` in tests via + ``Path.home()`` → ``/root``; unprivileged users get ``PermissionError`` on + ``/root/.hermes/…`` probes. Missing/unreadable dirs are treated as absent. + """ + try: + return path.is_dir() + except OSError: + return False + + def _build_service_path_dirs(project_root: Path | None = None) -> list[str]: """Build PATH directory list for service units, excluding non-existent dirs.""" if project_root is None: @@ -2111,21 +2124,21 @@ def _build_service_path_dirs(project_root: Path | None = None) -> list[str]: candidates = [] venv_bin = project_root / "venv" / "bin" - if venv_bin.is_dir(): + if _path_usable_bindir(venv_bin): candidates.append(str(venv_bin)) elif sys.prefix != sys.base_prefix: candidates.append(str(Path(sys.prefix) / "bin")) node_bin = project_root / "node_modules" / ".bin" - if node_bin.is_dir(): + if _path_usable_bindir(node_bin): candidates.append(str(node_bin)) hermes_home = get_hermes_home() hermes_node = hermes_home / "node" / "bin" - if hermes_node.is_dir(): + if _path_usable_bindir(hermes_node): candidates.append(str(hermes_node)) hermes_nm = hermes_home / "node_modules" / ".bin" - if hermes_nm.is_dir(): + if _path_usable_bindir(hermes_nm): candidates.append(str(hermes_nm)) return candidates From 16ff9464a5daae9b82bf2ce2c7de5ba8f80cfd40 Mon Sep 17 00:00:00 2001 From: Austin Pickett Date: Sat, 16 May 2026 00:04:58 -0400 Subject: [PATCH 4/4] Revert "fix(cli): tolerate unreadable dirs when building systemd PATH" This reverts commit 965610f922be5b2afb6fa412205077486734a433. --- hermes_cli/gateway.py | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/hermes_cli/gateway.py b/hermes_cli/gateway.py index f2d6223f3de..a865bcaf8be 100644 --- a/hermes_cli/gateway.py +++ b/hermes_cli/gateway.py @@ -2103,19 +2103,6 @@ def _hermes_home_for_target_user(target_home_dir: str) -> str: return str(current_hermes) -def _path_usable_bindir(path: Path) -> bool: - """True iff ``path`` exists as a dir and we could stat/read it. - - systemd unit generation may run under simulated ``sudo`` in tests via - ``Path.home()`` → ``/root``; unprivileged users get ``PermissionError`` on - ``/root/.hermes/…`` probes. Missing/unreadable dirs are treated as absent. - """ - try: - return path.is_dir() - except OSError: - return False - - def _build_service_path_dirs(project_root: Path | None = None) -> list[str]: """Build PATH directory list for service units, excluding non-existent dirs.""" if project_root is None: @@ -2124,21 +2111,21 @@ def _build_service_path_dirs(project_root: Path | None = None) -> list[str]: candidates = [] venv_bin = project_root / "venv" / "bin" - if _path_usable_bindir(venv_bin): + if venv_bin.is_dir(): candidates.append(str(venv_bin)) elif sys.prefix != sys.base_prefix: candidates.append(str(Path(sys.prefix) / "bin")) node_bin = project_root / "node_modules" / ".bin" - if _path_usable_bindir(node_bin): + if node_bin.is_dir(): candidates.append(str(node_bin)) hermes_home = get_hermes_home() hermes_node = hermes_home / "node" / "bin" - if _path_usable_bindir(hermes_node): + if hermes_node.is_dir(): candidates.append(str(hermes_node)) hermes_nm = hermes_home / "node_modules" / ".bin" - if _path_usable_bindir(hermes_nm): + if hermes_nm.is_dir(): candidates.append(str(hermes_nm)) return candidates