fix(kanban): make creator wake turns graph-safe
Carry the worker's completion handoff into the synthetic creator wake turn and label it as an automatic notification with inspect-the-board / don't-recreate guidance, so a woken orchestrator doesn't re-decompose work that already exists (#70752). Salvaged from PR #71100 by @yinkev; ported onto the restructured wake region (delivery_mode gating, scope_id, sub chat_id destinations). The auto_subscribe_on_create config-default half of the original PR was dropped as already superseded on main.
This commit is contained in:
@@ -467,6 +467,11 @@ class GatewayKanbanWatchersMixin:
|
|||||||
mode = sub.get("delivery_mode") or "notify"
|
mode = sub.get("delivery_mode") or "notify"
|
||||||
wake_agent = mode in ("notify+wake", "wake")
|
wake_agent = mode in ("notify+wake", "wake")
|
||||||
send_passive = mode != "wake"
|
send_passive = mode != "wake"
|
||||||
|
# Worker handoff carried into the synthetic wake turn below
|
||||||
|
# (#70752): without it the woken creator only sees
|
||||||
|
# "Task X completed" and re-decomposes work that already
|
||||||
|
# exists on the board.
|
||||||
|
wake_handoff = ""
|
||||||
for ev in d["events"]:
|
for ev in d["events"]:
|
||||||
kind = ev.kind
|
kind = ev.kind
|
||||||
# Identity prefix: attribute terminal pings to the
|
# Identity prefix: attribute terminal pings to the
|
||||||
@@ -488,10 +493,12 @@ class GatewayKanbanWatchersMixin:
|
|||||||
lines = payload_summary.strip().splitlines()
|
lines = payload_summary.strip().splitlines()
|
||||||
h = lines[0][:200] if lines else payload_summary[:200]
|
h = lines[0][:200] if lines else payload_summary[:200]
|
||||||
handoff = f"\n{h}"
|
handoff = f"\n{h}"
|
||||||
|
wake_handoff = h
|
||||||
elif task and task.result:
|
elif task and task.result:
|
||||||
lines = task.result.strip().splitlines()
|
lines = task.result.strip().splitlines()
|
||||||
r = lines[0][:160] if lines else task.result[:160]
|
r = lines[0][:160] if lines else task.result[:160]
|
||||||
handoff = f"\n{r}"
|
handoff = f"\n{r}"
|
||||||
|
wake_handoff = r
|
||||||
msg = (
|
msg = (
|
||||||
f"✔ {board_tag}{tag}Kanban {sub['task_id']} done"
|
f"✔ {board_tag}{tag}Kanban {sub['task_id']} done"
|
||||||
f" — {title}{handoff}"
|
f" — {title}{handoff}"
|
||||||
@@ -746,6 +753,19 @@ class GatewayKanbanWatchersMixin:
|
|||||||
assignee=_assignee,
|
assignee=_assignee,
|
||||||
board=board_slug,
|
board=board_slug,
|
||||||
)
|
)
|
||||||
|
# Graph-safe wake turn (#70752): carry the worker's
|
||||||
|
# completion handoff into the synthetic turn and
|
||||||
|
# label it as an automatic notification so the woken
|
||||||
|
# creator inspects the board instead of
|
||||||
|
# re-decomposing work that already exists.
|
||||||
|
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:
|
if not _is_push_adapter and _wake_kinds and _session_key:
|
||||||
# Wake self-post IS the delivery on this path —
|
# Wake self-post IS the delivery on this path —
|
||||||
|
|||||||
@@ -169,6 +169,8 @@ gateway:
|
|||||||
status_default: "status changed"
|
status_default: "status changed"
|
||||||
status_joiner: ", "
|
status_joiner: ", "
|
||||||
message: "[kanban] Task {task_id} {status}.\nTitle: {title}\nAssignee: @{assignee}\nBoard: {board}\n\nCheck the result or decide the next step."
|
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:
|
personality:
|
||||||
none_configured: "Geen persoonlikhede opgestel in `{path}/config.yaml` nie"
|
none_configured: "Geen persoonlikhede opgestel in `{path}/config.yaml` nie"
|
||||||
|
|||||||
@@ -192,6 +192,8 @@ gateway:
|
|||||||
status_default: "تغيّرت الحالة"
|
status_default: "تغيّرت الحالة"
|
||||||
status_joiner: "، "
|
status_joiner: "، "
|
||||||
message: "[kanban] المهمة {task_id} {status}.\nالعنوان: {title}\nالمُكلَّف: @{assignee}\nاللوحة: {board}\n\nراجع النتيجة أو قرّر الخطوة التالية."
|
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:
|
personality:
|
||||||
none_configured: "لا توجد شخصيات مُهيّأة في `{path}/config.yaml`"
|
none_configured: "لا توجد شخصيات مُهيّأة في `{path}/config.yaml`"
|
||||||
|
|||||||
@@ -169,6 +169,8 @@ gateway:
|
|||||||
status_default: "status changed"
|
status_default: "status changed"
|
||||||
status_joiner: ", "
|
status_joiner: ", "
|
||||||
message: "[kanban] Task {task_id} {status}.\nTitle: {title}\nAssignee: @{assignee}\nBoard: {board}\n\nCheck the result or decide the next step."
|
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:
|
personality:
|
||||||
none_configured: "Keine Persönlichkeiten in `{path}/config.yaml` konfiguriert"
|
none_configured: "Keine Persönlichkeiten in `{path}/config.yaml` konfiguriert"
|
||||||
|
|||||||
@@ -184,6 +184,8 @@ gateway:
|
|||||||
status_default: "status changed"
|
status_default: "status changed"
|
||||||
status_joiner: ", "
|
status_joiner: ", "
|
||||||
message: "[kanban] Task {task_id} {status}.\nTitle: {title}\nAssignee: @{assignee}\nBoard: {board}\n\nCheck the result or decide the next step."
|
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:
|
personality:
|
||||||
none_configured: "No personalities configured in `{path}/config.yaml`"
|
none_configured: "No personalities configured in `{path}/config.yaml`"
|
||||||
|
|||||||
@@ -169,6 +169,8 @@ gateway:
|
|||||||
status_default: "status changed"
|
status_default: "status changed"
|
||||||
status_joiner: ", "
|
status_joiner: ", "
|
||||||
message: "[kanban] Task {task_id} {status}.\nTitle: {title}\nAssignee: @{assignee}\nBoard: {board}\n\nCheck the result or decide the next step."
|
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:
|
personality:
|
||||||
none_configured: "No hay personalidades configuradas en `{path}/config.yaml`"
|
none_configured: "No hay personalidades configuradas en `{path}/config.yaml`"
|
||||||
|
|||||||
@@ -169,6 +169,8 @@ gateway:
|
|||||||
status_default: "status changed"
|
status_default: "status changed"
|
||||||
status_joiner: ", "
|
status_joiner: ", "
|
||||||
message: "[kanban] Task {task_id} {status}.\nTitle: {title}\nAssignee: @{assignee}\nBoard: {board}\n\nCheck the result or decide the next step."
|
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:
|
personality:
|
||||||
none_configured: "Aucune personnalité configurée dans `{path}/config.yaml`"
|
none_configured: "Aucune personnalité configurée dans `{path}/config.yaml`"
|
||||||
|
|||||||
@@ -173,6 +173,8 @@ gateway:
|
|||||||
status_default: "status changed"
|
status_default: "status changed"
|
||||||
status_joiner: ", "
|
status_joiner: ", "
|
||||||
message: "[kanban] Task {task_id} {status}.\nTitle: {title}\nAssignee: @{assignee}\nBoard: {board}\n\nCheck the result or decide the next step."
|
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:
|
personality:
|
||||||
none_configured: "Níl aon phearsantachtaí cumraithe in `{path}/config.yaml`"
|
none_configured: "Níl aon phearsantachtaí cumraithe in `{path}/config.yaml`"
|
||||||
|
|||||||
@@ -169,6 +169,8 @@ gateway:
|
|||||||
status_default: "status changed"
|
status_default: "status changed"
|
||||||
status_joiner: ", "
|
status_joiner: ", "
|
||||||
message: "[kanban] Task {task_id} {status}.\nTitle: {title}\nAssignee: @{assignee}\nBoard: {board}\n\nCheck the result or decide the next step."
|
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:
|
personality:
|
||||||
none_configured: "Nincs személyiség beállítva itt: `{path}/config.yaml`"
|
none_configured: "Nincs személyiség beállítva itt: `{path}/config.yaml`"
|
||||||
|
|||||||
@@ -169,6 +169,8 @@ gateway:
|
|||||||
status_default: "status changed"
|
status_default: "status changed"
|
||||||
status_joiner: ", "
|
status_joiner: ", "
|
||||||
message: "[kanban] Task {task_id} {status}.\nTitle: {title}\nAssignee: @{assignee}\nBoard: {board}\n\nCheck the result or decide the next step."
|
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:
|
personality:
|
||||||
none_configured: "Nessuna personalità configurata in `{path}/config.yaml`"
|
none_configured: "Nessuna personalità configurata in `{path}/config.yaml`"
|
||||||
|
|||||||
@@ -169,6 +169,8 @@ gateway:
|
|||||||
status_default: "status changed"
|
status_default: "status changed"
|
||||||
status_joiner: ", "
|
status_joiner: ", "
|
||||||
message: "[kanban] Task {task_id} {status}.\nTitle: {title}\nAssignee: @{assignee}\nBoard: {board}\n\nCheck the result or decide the next step."
|
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:
|
personality:
|
||||||
none_configured: "`{path}/config.yaml` に人格が設定されていません"
|
none_configured: "`{path}/config.yaml` に人格が設定されていません"
|
||||||
|
|||||||
@@ -169,6 +169,8 @@ gateway:
|
|||||||
status_default: "status changed"
|
status_default: "status changed"
|
||||||
status_joiner: ", "
|
status_joiner: ", "
|
||||||
message: "[kanban] Task {task_id} {status}.\nTitle: {title}\nAssignee: @{assignee}\nBoard: {board}\n\nCheck the result or decide the next step."
|
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:
|
personality:
|
||||||
none_configured: "`{path}/config.yaml`에 구성된 성격이 없습니다"
|
none_configured: "`{path}/config.yaml`에 구성된 성격이 없습니다"
|
||||||
|
|||||||
@@ -169,6 +169,8 @@ gateway:
|
|||||||
status_default: "status changed"
|
status_default: "status changed"
|
||||||
status_joiner: ", "
|
status_joiner: ", "
|
||||||
message: "[kanban] Task {task_id} {status}.\nTitle: {title}\nAssignee: @{assignee}\nBoard: {board}\n\nCheck the result or decide the next step."
|
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:
|
personality:
|
||||||
none_configured: "Nenhuma personalidade configurada em `{path}/config.yaml`"
|
none_configured: "Nenhuma personalidade configurada em `{path}/config.yaml`"
|
||||||
|
|||||||
@@ -169,6 +169,8 @@ gateway:
|
|||||||
status_default: "status changed"
|
status_default: "status changed"
|
||||||
status_joiner: ", "
|
status_joiner: ", "
|
||||||
message: "[kanban] Task {task_id} {status}.\nTitle: {title}\nAssignee: @{assignee}\nBoard: {board}\n\nCheck the result or decide the next step."
|
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:
|
personality:
|
||||||
none_configured: "В `{path}/config.yaml` не настроено ни одной личности"
|
none_configured: "В `{path}/config.yaml` не настроено ни одной личности"
|
||||||
|
|||||||
@@ -169,6 +169,8 @@ gateway:
|
|||||||
status_default: "status changed"
|
status_default: "status changed"
|
||||||
status_joiner: ", "
|
status_joiner: ", "
|
||||||
message: "[kanban] Task {task_id} {status}.\nTitle: {title}\nAssignee: @{assignee}\nBoard: {board}\n\nCheck the result or decide the next step."
|
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:
|
personality:
|
||||||
none_configured: "`{path}/config.yaml` içinde yapılandırılmış kişilik yok"
|
none_configured: "`{path}/config.yaml` içinde yapılandırılmış kişilik yok"
|
||||||
|
|||||||
@@ -169,6 +169,8 @@ gateway:
|
|||||||
status_default: "status changed"
|
status_default: "status changed"
|
||||||
status_joiner: ", "
|
status_joiner: ", "
|
||||||
message: "[kanban] Task {task_id} {status}.\nTitle: {title}\nAssignee: @{assignee}\nBoard: {board}\n\nCheck the result or decide the next step."
|
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:
|
personality:
|
||||||
none_configured: "У `{path}/config.yaml` не налаштовано жодної особистості"
|
none_configured: "У `{path}/config.yaml` не налаштовано жодної особистості"
|
||||||
|
|||||||
@@ -169,6 +169,8 @@ gateway:
|
|||||||
status_default: "status changed"
|
status_default: "status changed"
|
||||||
status_joiner: ", "
|
status_joiner: ", "
|
||||||
message: "[kanban] Task {task_id} {status}.\nTitle: {title}\nAssignee: @{assignee}\nBoard: {board}\n\nCheck the result or decide the next step."
|
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:
|
personality:
|
||||||
none_configured: "`{path}/config.yaml` 中未設定人格"
|
none_configured: "`{path}/config.yaml` 中未設定人格"
|
||||||
|
|||||||
@@ -169,6 +169,8 @@ gateway:
|
|||||||
status_default: "状态变化"
|
status_default: "状态变化"
|
||||||
status_joiner: ","
|
status_joiner: ","
|
||||||
message: "[kanban] 任务 {task_id} {status}。\n标题: {title}\n执行者: @{assignee}\n看板: {board}\n\n请检查结果或决定下一步动作。"
|
message: "[kanban] 任务 {task_id} {status}。\n标题: {title}\n执行者: @{assignee}\n看板: {board}\n\n请检查结果或决定下一步动作。"
|
||||||
|
handoff: "结果:{summary}"
|
||||||
|
guidance: "这是自动任务状态通知,不是再次分解任务的请求。创建后续任务前请先检查当前看板;不要重复创建已存在的任务或任务图。"
|
||||||
|
|
||||||
personality:
|
personality:
|
||||||
none_configured: "`{path}/config.yaml` 中未配置人格设定"
|
none_configured: "`{path}/config.yaml` 中未配置人格设定"
|
||||||
|
|||||||
@@ -128,7 +128,14 @@ def test_apiserver_sub_wakes_subscription_destination_via_self_post(tmp_path, mo
|
|||||||
assert len(posts) == 1
|
assert len(posts) == 1
|
||||||
assert posts[0]["session_id"] == "origin-session"
|
assert posts[0]["session_id"] == "origin-session"
|
||||||
assert all(post["session_id"] != "worker-session" for post in posts)
|
assert all(post["session_id"] != "worker-session" for post in posts)
|
||||||
assert tid in posts[0]["text"]
|
wake_text = posts[0]["text"]
|
||||||
|
assert tid in wake_text
|
||||||
|
# Graph-safe wake turn (#70752): the synthetic turn must carry the
|
||||||
|
# worker's completion handoff and the don't-recreate guidance so a
|
||||||
|
# woken orchestrator doesn't re-decompose existing work.
|
||||||
|
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
|
# The wake self-post IS the delivery on this path (no separate text-ping
|
||||||
# fallback is attempted for stateless api_server subs) — cursor advances
|
# fallback is attempted for stateless api_server subs) — cursor advances
|
||||||
# once the wake succeeds.
|
# once the wake succeeds.
|
||||||
|
|||||||
@@ -597,6 +597,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.
|
**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.
|
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).
|
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).
|
||||||
@@ -607,11 +609,11 @@ Config knobs (all under `kanban:` in `~/.hermes/config.yaml`):
|
|||||||
|
|
||||||
| Key | Default | Purpose |
|
| 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. |
|
| `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. |
|
| `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. |
|
| `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:
|
And the two auxiliary LLM slots:
|
||||||
|
|
||||||
|
|||||||
+4
-2
@@ -421,6 +421,8 @@ hermes dashboard # 导航栏中出现 "Kanban" 标签页,位于 "Skills
|
|||||||
|
|
||||||
**手动** —— `kanban.auto_decompose: false`。分诊任务保持在分诊中,直到你操作。点击卡片上的 **⚗ Decompose** 按钮,运行 `hermes kanban decompose <id>`(或 `--all`),或从聊天中使用 `/kanban decompose <id>`。这与看板的预分解器行为一致,适合需要完全控制运行时机的场景。
|
**手动** —— `kanban.auto_decompose: false`。分诊任务保持在分诊中,直到你操作。点击卡片上的 **⚗ Decompose** 按钮,运行 `hermes kanban decompose <id>`(或 `--all`),或从聊天中使用 `/kanban decompose <id>`。这与看板的预分解器行为一致,适合需要完全控制运行时机的场景。
|
||||||
|
|
||||||
|
**重要边界:** 手动模式只会关闭内置的 Triage 分解器。它不会阻止配置文件调用 `kanban_create`,也不会关闭创建者会话的唤醒。启用 `kanban.auto_subscribe_on_create: true` 时,任务的终止事件会通过合成状态消息恢复原始 agent,使其读取交接并判断是否确实需要新的后续工作。希望任务完成保持被动时,请设置 `auto_subscribe_on_create: false`。溯源时,内置分解器创建的子任务标记为 `created_by=auto-decomposer`;由恢复后的配置文件创建的任务则记录该配置文件名。
|
||||||
|
|
||||||
从 kanban 页面顶部的 **Orchestration: Auto/Manual** 切换按钮(翠绿色 = 自动,静音灰色 = 手动)在两种模式之间切换,或直接编辑 `config.yaml`。两种模式都与 `hermes kanban specify` 共存 —— 当你不想扇出时,它仍然可用作单任务规格重写。
|
从 kanban 页面顶部的 **Orchestration: Auto/Manual** 切换按钮(翠绿色 = 自动,静音灰色 = 手动)在两种模式之间切换,或直接编辑 `config.yaml`。两种模式都与 `hermes kanban specify` 共存 —— 当你不想扇出时,它仍然可用作单任务规格重写。
|
||||||
|
|
||||||
分解器的路由决策依赖于配置文件描述,这是一个每配置文件的标签原语,通过 `hermes profile create --description "..."`、`hermes profile describe <name> --text "..."`、`hermes profile describe <name> --auto`(LLM 从配置文件安装的 skill + 模型自动生成),或仪表盘展开的 **Orchestration settings** 面板中的每配置文件编辑器来设置。没有描述的配置文件仍然出现在名册中 —— 它们可以按名称路由,只是精度较低。分解器**绝不**会将子任务落地为 `assignee=None`:当 LLM 选择未知配置文件时,子任务路由到 `kanban.default_assignee`(如果未设置,则路由到活动默认配置文件)。
|
分解器的路由决策依赖于配置文件描述,这是一个每配置文件的标签原语,通过 `hermes profile create --description "..."`、`hermes profile describe <name> --text "..."`、`hermes profile describe <name> --auto`(LLM 从配置文件安装的 skill + 模型自动生成),或仪表盘展开的 **Orchestration settings** 面板中的每配置文件编辑器来设置。没有描述的配置文件仍然出现在名册中 —— 它们可以按名称路由,只是精度较低。分解器**绝不**会将子任务落地为 `assignee=None`:当 LLM 选择未知配置文件时,子任务路由到 `kanban.default_assignee`(如果未设置,则路由到活动默认配置文件)。
|
||||||
@@ -429,11 +431,11 @@ hermes dashboard # 导航栏中出现 "Kanban" 标签页,位于 "Skills
|
|||||||
|
|
||||||
| 键 | 默认值 | 用途 |
|
| 键 | 默认值 | 用途 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `auto_decompose` | `true` | 调度器每 tick 自动运行分解器。 |
|
| `auto_decompose` | `true` | 调度器每 tick 为 Triage 任务运行内置分解器;它不会限制配置文件驱动的 `kanban_create` 或创建者唤醒回合。 |
|
||||||
| `auto_decompose_per_tick` | `3` | 每个调度器 tick 的分解上限。超出部分推迟到下一个 tick。 |
|
| `auto_decompose_per_tick` | `3` | 每个调度器 tick 的分解上限。超出部分推迟到下一个 tick。 |
|
||||||
| `orchestrator_profile` | `""` | 拥有分解权的配置文件。空 = 回退到活动默认配置文件。 |
|
| `orchestrator_profile` | `""` | 拥有分解权的配置文件。空 = 回退到活动默认配置文件。 |
|
||||||
| `default_assignee` | `""` | LLM 选择未知配置文件时子任务的落地位置。空 = 回退到活动默认配置文件。 |
|
| `default_assignee` | `""` | LLM 选择未知配置文件时子任务的落地位置。空 = 回退到活动默认配置文件。 |
|
||||||
| `auto_subscribe_on_create` | `true` | 当 worker 在具有持久投递通道的会话(消息网关或 TUI)内调用 `kanban_create` 时,原始会话会自动订阅新任务的完成/阻塞事件。调度器仍负责驱动投递 —— 此设置只决定调用者的聊天/密钥是否出现在通知订阅表中。设为 `false` 则要求对每个任务显式调用 `kanban_notify-subscribe`。 |
|
| `auto_subscribe_on_create` | `true` | 当 `kanban_create` 在持久 gateway/TUI 会话中运行时,终止事件会通过合成状态回合恢复原始 agent。设为 `false` 可让完成保持被动,或要求显式调用 `kanban_notify-subscribe`。此设置独立于 `auto_decompose`。 |
|
||||||
|
|
||||||
以及两个辅助 LLM 槽:
|
以及两个辅助 LLM 槽:
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user