Skip to content
Open
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
11 changes: 11 additions & 0 deletions gateway/kanban_watchers.py
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,7 @@ def _collect():
sub["task_id"], sub["platform"],
sub["chat_id"], sub.get("thread_id") or "",
)
wake_handoff = ""
for ev in d["events"]:
kind = ev.kind
# Identity prefix: attribute terminal pings to the
Expand All @@ -365,10 +366,12 @@ def _collect():
lines = payload_summary.strip().splitlines()
h = lines[0][:200] if lines else payload_summary[:200]
handoff = f"\n{h}"
wake_handoff = h
elif task and task.result:
lines = task.result.strip().splitlines()
r = lines[0][:160] if lines else task.result[:160]
handoff = f"\n{r}"
wake_handoff = r
msg = (
f"✔ {board_tag}{tag}Kanban {sub['task_id']} done"
f" — {title}{handoff}"
Expand Down Expand Up @@ -562,6 +565,14 @@ def _collect():
assignee=_assignee,
board=board_slug,
)
if wake_handoff:
_synth += "\n" + t(
"gateway.kanban.wake.handoff",
summary=wake_handoff,
)
_synth += "\n\n" + t(
"gateway.kanban.wake.guidance"
)

if not _is_push_adapter and _wake_kinds and _session_key:
# Wake self-post IS the delivery on this path —
Expand Down
23 changes: 8 additions & 15 deletions hermes_cli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -1547,21 +1547,6 @@ def _ensure_hermes_home_managed(home: Path):
# Example: 1800 = compact after 30 min idle.
},

# Kanban subsystem (orchestrator workers + dispatcher-driven child tasks).
# See tools/kanban_tools.py and hermes_cli/kanban_db.py for the actual
# implementations. Per-platform notification opt-out is handled by the
# kanban dashboard (see ``hermes dashboard`` -> Notifications).
"kanban": {
# Auto-subscribe the originating gateway/TUI session to task
# completion + block events when ``kanban_create`` is called from
# inside a session that has a persistent delivery channel. The
# agent that dispatched the task will get notified automatically
# instead of having to poll. Disable to mirror pre-feature
# behaviour — e.g. for a profile that prefers explicit
# ``kanban_notify-subscribe`` calls per task.
"auto_subscribe_on_create": True,
},

# Anthropic prompt caching (Claude via OpenRouter or native Anthropic API).
# cache_ttl must be "5m" or "1h" (Anthropic-supported tiers); other values are ignored.
"prompt_caching": {
Expand Down Expand Up @@ -2926,6 +2911,14 @@ def _ensure_hermes_home_managed(home: Path):
# assignee to any installed profile. When unset, falls back to the
# default profile. A task never ends up with assignee=None.
"default_assignee": "",
# Auto-subscribe the originating gateway/TUI session when
# ``kanban_create`` runs inside a persistent delivery channel. A
# terminal task event resumes that agent with a synthetic status turn
# instead of requiring polling. This is independent of
# ``auto_decompose``: the resumed profile may still call
# ``kanban_create`` for legitimate follow-up work. Disable to keep
# task completion passive or require explicit notify subscriptions.
"auto_subscribe_on_create": True,
# Per-profile concurrency cap (#21582). When set to a positive int,
# no single profile can have more than N workers running at once,
# even if the global max_in_progress / max_spawn caps would allow
Expand Down
2 changes: 2 additions & 0 deletions locales/af.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ gateway:
status_default: "status changed"
status_joiner: ", "
message: "[kanban] Task {task_id} {status}.\nTitle: {title}\nAssignee: @{assignee}\nBoard: {board}\n\nCheck the result or decide the next step."
handoff: "Result: {summary}"
guidance: "This is an automatic task-status notification, not a request to decompose the task again. Inspect the current board before creating follow-up tasks; do not recreate tasks or graphs that already exist."

personality:
none_configured: "Geen persoonlikhede opgestel in `{path}/config.yaml` nie"
Expand Down
2 changes: 2 additions & 0 deletions locales/ar.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,8 @@ gateway:
status_default: "تغيّرت الحالة"
status_joiner: "، "
message: "[kanban] المهمة {task_id} {status}.\nالعنوان: {title}\nالمُكلَّف: @{assignee}\nاللوحة: {board}\n\nراجع النتيجة أو قرّر الخطوة التالية."
handoff: "Result: {summary}"
guidance: "This is an automatic task-status notification, not a request to decompose the task again. Inspect the current board before creating follow-up tasks; do not recreate tasks or graphs that already exist."

personality:
none_configured: "لا توجد شخصيات مُهيّأة في `{path}/config.yaml`"
Expand Down
2 changes: 2 additions & 0 deletions locales/de.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ gateway:
status_default: "status changed"
status_joiner: ", "
message: "[kanban] Task {task_id} {status}.\nTitle: {title}\nAssignee: @{assignee}\nBoard: {board}\n\nCheck the result or decide the next step."
handoff: "Result: {summary}"
guidance: "This is an automatic task-status notification, not a request to decompose the task again. Inspect the current board before creating follow-up tasks; do not recreate tasks or graphs that already exist."

personality:
none_configured: "Keine Persönlichkeiten in `{path}/config.yaml` konfiguriert"
Expand Down
2 changes: 2 additions & 0 deletions locales/en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,8 @@ gateway:
status_default: "status changed"
status_joiner: ", "
message: "[kanban] Task {task_id} {status}.\nTitle: {title}\nAssignee: @{assignee}\nBoard: {board}\n\nCheck the result or decide the next step."
handoff: "Result: {summary}"
guidance: "This is an automatic task-status notification, not a request to decompose the task again. Inspect the current board before creating follow-up tasks; do not recreate tasks or graphs that already exist."

personality:
none_configured: "No personalities configured in `{path}/config.yaml`"
Expand Down
2 changes: 2 additions & 0 deletions locales/es.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ gateway:
status_default: "status changed"
status_joiner: ", "
message: "[kanban] Task {task_id} {status}.\nTitle: {title}\nAssignee: @{assignee}\nBoard: {board}\n\nCheck the result or decide the next step."
handoff: "Result: {summary}"
guidance: "This is an automatic task-status notification, not a request to decompose the task again. Inspect the current board before creating follow-up tasks; do not recreate tasks or graphs that already exist."

personality:
none_configured: "No hay personalidades configuradas en `{path}/config.yaml`"
Expand Down
2 changes: 2 additions & 0 deletions locales/fr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ gateway:
status_default: "status changed"
status_joiner: ", "
message: "[kanban] Task {task_id} {status}.\nTitle: {title}\nAssignee: @{assignee}\nBoard: {board}\n\nCheck the result or decide the next step."
handoff: "Result: {summary}"
guidance: "This is an automatic task-status notification, not a request to decompose the task again. Inspect the current board before creating follow-up tasks; do not recreate tasks or graphs that already exist."

personality:
none_configured: "Aucune personnalité configurée dans `{path}/config.yaml`"
Expand Down
2 changes: 2 additions & 0 deletions locales/ga.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,8 @@ gateway:
status_default: "status changed"
status_joiner: ", "
message: "[kanban] Task {task_id} {status}.\nTitle: {title}\nAssignee: @{assignee}\nBoard: {board}\n\nCheck the result or decide the next step."
handoff: "Result: {summary}"
guidance: "This is an automatic task-status notification, not a request to decompose the task again. Inspect the current board before creating follow-up tasks; do not recreate tasks or graphs that already exist."

personality:
none_configured: "Níl aon phearsantachtaí cumraithe in `{path}/config.yaml`"
Expand Down
2 changes: 2 additions & 0 deletions locales/hu.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ gateway:
status_default: "status changed"
status_joiner: ", "
message: "[kanban] Task {task_id} {status}.\nTitle: {title}\nAssignee: @{assignee}\nBoard: {board}\n\nCheck the result or decide the next step."
handoff: "Result: {summary}"
guidance: "This is an automatic task-status notification, not a request to decompose the task again. Inspect the current board before creating follow-up tasks; do not recreate tasks or graphs that already exist."

personality:
none_configured: "Nincs személyiség beállítva itt: `{path}/config.yaml`"
Expand Down
2 changes: 2 additions & 0 deletions locales/it.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ gateway:
status_default: "status changed"
status_joiner: ", "
message: "[kanban] Task {task_id} {status}.\nTitle: {title}\nAssignee: @{assignee}\nBoard: {board}\n\nCheck the result or decide the next step."
handoff: "Result: {summary}"
guidance: "This is an automatic task-status notification, not a request to decompose the task again. Inspect the current board before creating follow-up tasks; do not recreate tasks or graphs that already exist."

personality:
none_configured: "Nessuna personalità configurata in `{path}/config.yaml`"
Expand Down
2 changes: 2 additions & 0 deletions locales/ja.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ gateway:
status_default: "status changed"
status_joiner: ", "
message: "[kanban] Task {task_id} {status}.\nTitle: {title}\nAssignee: @{assignee}\nBoard: {board}\n\nCheck the result or decide the next step."
handoff: "Result: {summary}"
guidance: "This is an automatic task-status notification, not a request to decompose the task again. Inspect the current board before creating follow-up tasks; do not recreate tasks or graphs that already exist."

personality:
none_configured: "`{path}/config.yaml` に人格が設定されていません"
Expand Down
2 changes: 2 additions & 0 deletions locales/ko.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ gateway:
status_default: "status changed"
status_joiner: ", "
message: "[kanban] Task {task_id} {status}.\nTitle: {title}\nAssignee: @{assignee}\nBoard: {board}\n\nCheck the result or decide the next step."
handoff: "Result: {summary}"
guidance: "This is an automatic task-status notification, not a request to decompose the task again. Inspect the current board before creating follow-up tasks; do not recreate tasks or graphs that already exist."

personality:
none_configured: "`{path}/config.yaml`에 구성된 성격이 없습니다"
Expand Down
2 changes: 2 additions & 0 deletions locales/pt.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ gateway:
status_default: "status changed"
status_joiner: ", "
message: "[kanban] Task {task_id} {status}.\nTitle: {title}\nAssignee: @{assignee}\nBoard: {board}\n\nCheck the result or decide the next step."
handoff: "Result: {summary}"
guidance: "This is an automatic task-status notification, not a request to decompose the task again. Inspect the current board before creating follow-up tasks; do not recreate tasks or graphs that already exist."

personality:
none_configured: "Nenhuma personalidade configurada em `{path}/config.yaml`"
Expand Down
2 changes: 2 additions & 0 deletions locales/ru.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ gateway:
status_default: "status changed"
status_joiner: ", "
message: "[kanban] Task {task_id} {status}.\nTitle: {title}\nAssignee: @{assignee}\nBoard: {board}\n\nCheck the result or decide the next step."
handoff: "Result: {summary}"
guidance: "This is an automatic task-status notification, not a request to decompose the task again. Inspect the current board before creating follow-up tasks; do not recreate tasks or graphs that already exist."

personality:
none_configured: "В `{path}/config.yaml` не настроено ни одной личности"
Expand Down
2 changes: 2 additions & 0 deletions locales/tr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ gateway:
status_default: "status changed"
status_joiner: ", "
message: "[kanban] Task {task_id} {status}.\nTitle: {title}\nAssignee: @{assignee}\nBoard: {board}\n\nCheck the result or decide the next step."
handoff: "Result: {summary}"
guidance: "This is an automatic task-status notification, not a request to decompose the task again. Inspect the current board before creating follow-up tasks; do not recreate tasks or graphs that already exist."

personality:
none_configured: "`{path}/config.yaml` içinde yapılandırılmış kişilik yok"
Expand Down
2 changes: 2 additions & 0 deletions locales/uk.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ gateway:
status_default: "status changed"
status_joiner: ", "
message: "[kanban] Task {task_id} {status}.\nTitle: {title}\nAssignee: @{assignee}\nBoard: {board}\n\nCheck the result or decide the next step."
handoff: "Result: {summary}"
guidance: "This is an automatic task-status notification, not a request to decompose the task again. Inspect the current board before creating follow-up tasks; do not recreate tasks or graphs that already exist."

personality:
none_configured: "У `{path}/config.yaml` не налаштовано жодної особистості"
Expand Down
2 changes: 2 additions & 0 deletions locales/zh-hant.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ gateway:
status_default: "status changed"
status_joiner: ", "
message: "[kanban] Task {task_id} {status}.\nTitle: {title}\nAssignee: @{assignee}\nBoard: {board}\n\nCheck the result or decide the next step."
handoff: "結果:{summary}"
guidance: "這是自動任務狀態通知,不是再次分解任務的請求。建立後續任務前請先檢查目前看板;不要重複建立已存在的任務或任務圖。"

personality:
none_configured: "`{path}/config.yaml` 中未設定人格"
Expand Down
2 changes: 2 additions & 0 deletions locales/zh.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ gateway:
status_default: "状态变化"
status_joiner: ","
message: "[kanban] 任务 {task_id} {status}。\n标题: {title}\n执行者: @{assignee}\n看板: {board}\n\n请检查结果或决定下一步动作。"
handoff: "结果:{summary}"
guidance: "这是自动任务状态通知,不是再次分解任务的请求。创建后续任务前请先检查当前看板;不要重复创建已存在的任务或任务图。"

personality:
none_configured: "`{path}/config.yaml` 中未配置人格设定"
Expand Down
6 changes: 5 additions & 1 deletion tests/gateway/test_kanban_notifier_apiserver_wake.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,11 @@ async def fake_self_post(adapter, *, text, session_id):
)
assert len(posts) == 1
assert posts[0]["session_id"] == "raw-sid-123"
assert tid in posts[0]["text"]
wake_text = posts[0]["text"]
assert tid in wake_text
assert "done once" in wake_text, "creator wake must carry the worker handoff"
assert "not a request to decompose" in wake_text.lower()
assert "do not recreate" in wake_text.lower()
# The wake self-post IS the delivery on this path (no separate text-ping
# fallback is attempted for stateless api_server subs) — cursor advances
# once the wake succeeds.
Expand Down
1 change: 1 addition & 0 deletions tests/hermes_cli/test_set_config_value.py
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,7 @@ class TestValidateConfigKey:
"platforms.discord.enabled",
"gateway.platforms.my_platform.extra.token",
"approvals.mode",
"kanban.auto_subscribe_on_create",
])
def test_known_keys_pass(self, key):
from hermes_cli.config import _validate_config_key
Expand Down
6 changes: 4 additions & 2 deletions website/docs/user-guide/features/kanban.md
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,8 @@ The kanban board has two ways to handle a task you drop into the Triage column:

**Manual** — `kanban.auto_decompose: false`. Triage tasks stay in triage until you act. Click the **⚗ Decompose** button on a card, run `hermes kanban decompose <id>` (or `--all`), or use `/kanban decompose <id>` from a chat. This matches the pre-decomposer behavior of the board, useful when you want full control over what runs when.

**Important boundary:** Manual mode disables only the built-in Triage decomposer. It does not prevent a profile from calling `kanban_create`, and it does not disable creator-session wake-ups. With `kanban.auto_subscribe_on_create: true`, a task's terminal event resumes the originating agent with a synthetic status turn so it can inspect the handoff and decide whether genuinely new follow-up work is needed. Set `auto_subscribe_on_create: false` when task completion should remain passive. For provenance, built-in decomposer children use `created_by=auto-decomposer`; tasks created by a resumed profile carry that profile name instead.

Flip between the two modes from the **Orchestration: Auto/Manual** pill at the top of the kanban page (emerald = Auto, muted gray = Manual), or by editing `config.yaml` directly. Both modes coexist with `hermes kanban specify` — that's still available as a single-task spec rewrite when you don't want fan-out.

The decomposer's routing decisions depend on profile descriptions, which is a per-profile labeling primitive you set with `hermes profile create --description "..."`, `hermes profile describe <name> --text "..."`, `hermes profile describe <name> --auto` (LLM-generates from the profile's installed skills + model), or the dashboard's per-profile editor in the expanded **Orchestration settings** panel. Profiles without a description still appear in the roster — they're routable by name, just less precisely. The decomposer NEVER lands a child task with `assignee=None`: when the LLM picks an unknown profile, the child gets routed to `kanban.default_assignee` (or the active default profile if that's unset).
Expand All @@ -545,11 +547,11 @@ Config knobs (all under `kanban:` in `~/.hermes/config.yaml`):

| Key | Default | Purpose |
|---|---|---|
| `auto_decompose` | `true` | Dispatcher auto-runs the decomposer every tick. |
| `auto_decompose` | `true` | Dispatcher auto-runs the built-in decomposer for Triage tasks every tick. It does not gate profile-driven `kanban_create` calls or creator wake turns. |
| `auto_decompose_per_tick` | `3` | Cap on decompositions per dispatcher tick. Excess defers to the next tick. |
| `orchestrator_profile` | `""` | Profile assigned to the root/orchestration task after decomposition. Empty = fall back to active default profile. |
| `default_assignee` | `""` | Where a child task lands when the LLM picks an unknown profile. Empty = fall back to active default. |
| `auto_subscribe_on_create` | `true` | When a worker calls `kanban_create` from inside a session with a persistent delivery channel (messaging gateway or TUI), the originating session is auto-subscribed to the new task's completion/block events. The dispatcher still drives the delivery — this only changes whether the caller's chat/key shows up in the notify-sub table. Set to `false` to require explicit `kanban_notify-subscribe` calls per task. |
| `auto_subscribe_on_create` | `true` | When `kanban_create` runs inside a persistent gateway/TUI session, terminal events resume that originating agent with a synthetic status turn. Set to `false` for passive completion or to require explicit `kanban_notify-subscribe` calls. Independent of `auto_decompose`. |

And the two auxiliary LLM slots:

Expand Down
Loading
Loading